# Config Typing 自动补全设计 (v2)

> **前置**: [registry-auto-registration.md](./registry-auto-registration.md)（自动注册机制）
> **目标**: 把发现配置错误的时间，从 "跑到那一行代码" 提前到 "写下那一行配置"。
> **定位**: 本项目 (`layer-registry`) 是一个通用 PyPI 包，为任意 Python 项目提供**自动注册 + 自动 config typing stub 生成**能力。`browseruse-bench` 仅是其消费者之一。
> **相关文档**: [specific_benchmark.md](./specific_benchmark.md)（browseruse-bench 消费者应用示例）

---

## 1. 设计灵魂

### 1.1 一句话定位

`__init__` 签名即配置契约 — 构造参数定义了合法配置的完整边界，任何越界在**编写时**就能发现、在**启动时**就会拒绝。

### 1.2 核心哲学

Config-driven 框架（mmcv/OpenMMLab、各类 benchmark runner）的共同痛点：

```
传统体验
════════
1. 写 config.yaml（盲写，无补全，翻文档或翻源码）
2. 启动程序（等几分钟拉起模型/浏览器/环境）
3. 跑了 N 个 step 或等了几分钟
4. 框架拉起某个模块，该模块的 __init__ 才收到配置
5. 不存在的参数 → 崩溃；类型错误 → 崩溃
6. 修改 config，回到 step 1

浪费的时间 = step 2-4 的全部等待时间
根因 = 配置和代码的契约在运行到那一行代码之前无法校验
```

**本包的存在意义就是消灭这个等待。** 两个手段，服务同一个目标：

| 手段 | 时机 | 效果 |
|------|------|------|
| **IDE 补全**（Python stub + JSON Schema） | 编写时 | 写下字段名的瞬间就知道是否合法 |
| **Pydantic 全量校验** | 程序入口 `load_config()` | 启动即校验，不进入任何业务逻辑就拒绝非法配置 |

两者共同保证：**如果程序走过了 `load_config()` 这一行，后续不可能因为配置参数问题崩溃。**

### 1.3 核心用户

**核心用户是 benchmark / 框架的使用者（Bob），不是开发者。**

Bob 的画像：
- 做实验的研究员 / 工程师
- 通过 YAML 或 Python 配置驱动实验
- **不读实现源码** — 他不知道 `ChatOpenAI.__init__` 接受什么参数
- 只想在 IDE 里输入 `provider:` 后看到 `openai | anthropic | deepseek`
- 选了 `openai` 后看到 `model_id`, `temperature`, `max_tokens` 的提示和 description
- 填了 typo 字段立刻看到红线或启动时报错

框架中有三类角色参与 config-typing，但权重完全不同：

| 角色 | 在做什么 | 与 config-typing 的关系 |
|------|---------|----------------------|
| **实现者** | 写 `ChatOpenAI.__init__(self, *, model_id, ...)` | **被动参与** — 不需要做任何额外的事 |
| **框架开发者** | 设置 registrar，commit 生成的 stub 文件 | **一次性设置** — 配好后自动化 |
| **配置使用者 (Bob)** | 写 `config.yaml` 或 Python `BenchConfig(llm=...)` | **核心受益者** — 零知识负担的配置编写 |

**设计优先级**：Bob 的体验是第一优先。实现者零负担是底线约束。

### 1.4 目标场景：端到端故事

```
Bob 的完整体验流
════════════════════════════════════════════════════════════

Day 1: 使用框架内建实现
──────────────────────
1. pip install alice-bench
2. 仓库里已有 generated/ 下的 stub 文件（框架开发者 commit 的）
3. 打开 IDE，写 experiment.yaml:
     llm:
       provider: █               ← YAML 提示: openai | anthropic | deepseek
       provider: openai
       model_id: █               ← 提示: str, 描述 "模型 ID，如 gpt-4o"
       temperature: █            ← 提示: float, default 0.0, ge=0, le=2
       typo_field: 123           ← 红线: 未知字段 (OpenAI 无 **kwargs)
4. 运行:
     bench run --config experiment.yaml
     │
     ├─ discover() → 注册表指纹未变 → 跳过 stub 更新
     ├─ load_config() → 从注册表构建 union → Pydantic 全量校验
     │   └─ 如有 typo_field → ConfigurationError (程序未进入任何业务逻辑)
     └─ 校验通过 → 开始跑 benchmark

Day 2: Bob 自己写了一个 LLM Provider
─────────────────────────────────────
5. Bob 写了:
     class ChatLocal(ChatBaseModel):
         def __init__(self, *, model_path: str, quantize: bool = False):
             ...
   → import-time 自动注册为 "chat_local"

6. Bob 运行:
     bench run --config local_experiment.yaml
     │
     ├─ discover() → 注册表多了 "chat_local" → 指纹变化 → 自动重新生成 stubs
     ├─ load_config() → 从当前注册表构建 union (已含 chat_local) → 校验通过
     └─ 开始跑 benchmark

7. Bob 下次打开 IDE:
     llm:
       provider: █       ← 提示: openai | anthropic | deepseek | chat_local ✓
       provider: chat_local
       model_path: █     ← 提示: str (必填) ✓
       quantize: █       ← 提示: bool, default False ✓

整个过程 Bob 没有:
  ✗ 读 ChatOpenAI 或 ChatLocal 的源码
  ✗ 读框架文档的 "如何配置" 章节
  ✗ 手动运行任何 generate 命令
  ✗ 等到 benchmark 跑了一半才发现配置写错
```

### 1.5 两条使用路径

**路径 1: YAML config + CLI（日常路径，认知成本最低）**

大多数 benchmark 框架提供 CLI，用户通过 YAML 配置驱动实验。这是 Bob 最常用的方式：

```yaml
# experiment.yaml — Bob 的日常工作
llm:
  provider: openai          # ← YAML Language Server 提示合法值
  model_id: gpt-4o          # ← 提示: str, description "模型 ID"
  temperature: 0.5          # ← 提示: float, ge=0, le=2

agent:
  name: browser_use
  max_steps: 50
```

```bash
bench run --config experiment.yaml
```

**路径 2: Python 脚本（进阶路径，服务自定义场景）**

当 Bob 有 CLI 不支持的需求（自定义跑测逻辑、批量参数扫描等）：

```python
from generated import LlmConfig, OpenaiLLMConfig

# IDE 补全: model_id, temperature, max_tokens ✓
config = OpenaiLLMConfig(model_id="gpt-4o", temperture=0.5)
#                                           ^^^^^^^^^ IDE 红线: typo

# 自定义跑测逻辑
for temp in [0.0, 0.5, 1.0]:
    config = OpenaiLLMConfig(model_id="gpt-4o", temperature=temp)
    run_benchmark(config)
```

**两种输出都是必要的**：YAML stub (JSON Schema) 服务路径 1，Python stub 服务路径 2。

### 1.6 Open/Closed Schema

提取器检测 `__init__` 是否包含 `**kwargs` / `*args`，决定 schema 的开放/封闭性：

```python
# Closed schema: 无 **kwargs → extra="forbid" → 严格校验
class ChatOpenAI(ChatBaseModel):
    def __init__(self, *, model_id: str, temperature: float = 0.0):
        ...
# → 用户写了 top_k: 5 → ConfigurationError: "ChatOpenAI 不接受 'top_k'"
# → IDE 红线: 未知字段

# Open schema: 有 **kwargs → extra="allow" → 放行未知字段
class FlexibleProvider(ChatBaseModel):
    def __init__(self, *, model_id: str, **kwargs):
        ...
# → 用户写了 top_k: 5 → 放行，传入 **kwargs
# → IDE 补全只提示已声明的 model_id，但不红线未知字段
# → stub 注释标注: "此实现接受额外参数"
```

**核心原则**：无 `**kwargs` = 100% 配置校验；有 `**kwargs` = 部分校验（已声明字段校验，额外字段放行）。

**注意**：此检测仅作用于 Tier 1/2（签名反射路径）。Tier 3（显式 `__config_schema__`）直接尊重用户定义的 BaseModel 的 `model_config`，不做任何覆盖。用户既然显式声明了完整的 config schema，说明他已经完全掌控了 schema 的行为。

### 1.7 自动保鲜：discover() 驱动的按需更新

Stubs 和 runtime validation 是**两个分离的关注点**：

| 关注点 | 载体 | 目的 | 更新时机 |
|--------|------|------|---------|
| **Stubs** | 磁盘文件 (`.py` + `.schema.json`) | IDE 补全 | discover() 时按需更新 |
| **Runtime validation** | 内存中从注册表构建的 union type | fail-fast 校验 | load_config() 时实时构建 |

**Runtime validation 不依赖 stub 文件**。它直接从当前运行的注册表构建 union type。这保证了：
- 即使 stubs 过期，runtime 校验仍然是准确的
- 新增实现 → 第一次运行时 stubs 更新 → 下次打开 IDE 就有补全

**自动更新流程**：

```
discover("my_app.agents", "my_app.llm", ...)
    │
    ├─ 原有职责: import 模块 → 填充注册表
    │
    └─ 新增职责: 注册表指纹比对 → 按需更新 stubs
        │
        ├─ 计算当前注册表指纹 (keys + class qualnames + __init__ signatures)
        ├─ 与缓存指纹比较
        │   │
        │   ├─ 相同 → 跳过，零开销
        │   └─ 不同 → 自动重新生成
        │       ├─ Python stub 文件（IDE 补全）
        │       ├─ JSON Schema 文件（YAML 补全）
        │       └─ 更新缓存指纹
        │
        └─ 继续程序执行（不阻塞）
```

用户使用框架必然有 `discover()` 调用，因此自动更新**不需要任何额外配置**。

CLI 同时提供手动更新命令，供框架开发者在发布前强制重新生成：

```bash
layer-registry update-stubs --config layer-registry.yaml
```

### 1.8 设计优先级

| 优先级 | 内容 | 理由 |
|--------|------|------|
| **P0** | fail-fast 校验 | 核心价值；没有这个其他都是装饰 |
| **P1** | YAML 补全 | 日常路径的体验；降低认知成本 |
| **P2** | Python 类型补全 | 进阶路径；服务自定义脚本场景 |
| **P3** | 实现者零负担 | 底线约束；不能为了补全让实现者多写代码 |
| **P4** | 自动保鲜 | discover() 驱动的按需更新；用户无感 |

---

## 2. 核心问题

注册表已经知道：
- 所有合法的 key（`"openai"`, `"anthropic"`, `"lexmount"`, ...）
- 每个 key 对应的实现类（`ChatOpenAI`, `ChatAnthropic`, `LexmountProvider`, ...）

**缺失的一环**：注册表不知道每个实现类**接受什么配置参数**。

要实现 Section 1 描述的 fail-fast 体验，我们需要：
1. 一种**约定**让实现类声明自己的配置 schema（最好是零额外代码）
2. 一个**提取器**把这些 schema 从 `__init__` 签名中读取出来（包括 open/closed 判定）
3. 一个**构建器**把提取的 schema 按 layer 组装成 discriminated union
4. 两种**输出**：Python stub 文件（IDE 补全）+ JSON Schema（YAML 补全）
5. 一个**保鲜机制**确保 stubs 与注册表同步

### 2.1 v1 的问题

v1 方案要求实现类内嵌一个 `ConfigSchema(BaseModel)` 嵌套类：

```python
class ChatOpenAI(ChatBaseModel):
    class ConfigSchema(BaseModel):   # ← 每个实现都要定义嵌套类
        model_id: str = Field(...)
        temperature: float = 0.0
```

**问题**：
- 嵌套类模式冗长，增加认知负担
- 实现者必须同时维护 `ConfigSchema` 和 `__init__` 两处字段定义，容易不同步
- 作为 PyPI 包推广时，对使用者的侵入性太强
- **违反 "实现者零负担" 底线**

---

## 3. Config 声明方式 — 三级方案

**核心原则**：`__init__` 签名就是 config schema 的权威来源。不需要额外声明。

### 3.1 Tier 1: `__init__` 签名 — 零额外代码（默认路径）

实现者只需写普通的 `__init__`，提取器自动从签名中提取类型和默认值：

```python
class ChatOpenAI(ChatBaseModel):
    """自动注册为 "openai" """

    def __init__(self, *, model_id: str, temperature: float = 0.0,
                 max_tokens: int = 4096, top_p: float = 1.0):
        self.model_id = model_id
        self.temperature = temperature
        self.max_tokens = max_tokens
        self.top_p = top_p
```

提取结果：`{model_id: str, temperature: float=0.0, max_tokens: int=4096, top_p: float=1.0}`

无 `**kwargs` → schema closed → `extra="forbid"` → 用户多写字段立即报错。

**适用场景**：快速原型、第三方插件、大部分不需要 rich metadata 的实现。

### 3.2 Tier 1.5: `__init__` + Docstring — 零代码获得描述

实现者只需要写标准的 Google/NumPy/reST 格式 docstring，提取器自动从 `Args:` 段落提取参数描述：

```python
class ChatOpenAI(ChatBaseModel):
    """OpenAI LLM provider.

    Args:
        model_id: 模型 ID，如 gpt-4o
        temperature: 生成温度，0-2 之间
        max_tokens: 最大输出 token 数
        top_p: nucleus sampling 参数
    """

    def __init__(self, *, model_id: str, temperature: float = 0.0,
                 max_tokens: int = 4096, top_p: float = 1.0):
        ...
```

提取结果：
```
model_id: str, description="模型 ID，如 gpt-4o"       ← 来自 docstring
temperature: float = 0.0, description="生成温度，0-2 之间"  ← 来自 docstring
max_tokens: int = 4096, description="最大输出 token 数"    ← 来自 docstring
top_p: float = 1.0, description="nucleus sampling 参数"   ← 来自 docstring
```

**价值**：大幅缩小 Tier 1 和 Tier 2 的体验差距。实现者不需要学 `Annotated` 和 `Field`，只需要写常规 docstring（大部分人已经在写），Bob 就能在 YAML 补全中看到参数描述。

**描述来源优先级**：`Annotated[T, Field(description=...)]` > docstring `Args:` > 无描述

**实现**：可使用 `docstring_parser` 库（支持 Google/NumPy/reST 三种格式），作为可选依赖。

**适用场景**：已有 docstring 习惯的团队、不想引入 Pydantic `Field` 的轻量场景。

### 3.3 Tier 2: `__init__` + `Annotated[T, Field()]` — 带元数据（推荐路径）

需要 description、约束 (ge/le/gt) 时，在 `__init__` 类型标注中使用 `Annotated`：

```python
from typing import Annotated
from pydantic import Field

class ChatOpenAI(ChatBaseModel):
    """自动注册为 "openai" """

    def __init__(
        self, *,
        model_id: Annotated[str, Field(description="模型 ID，如 gpt-4o")],
        temperature: Annotated[float, Field(0.0, ge=0, le=2)] = 0.0,
        max_tokens: Annotated[int, Field(4096, gt=0)] = 4096,
        top_p: Annotated[float, Field(1.0, ge=0, le=1)] = 1.0,
        base_url: Annotated[str | None, Field(None, description="自定义 API endpoint")] = None,
    ):
        self.model_id = model_id
        self.temperature = temperature
        ...
```

**优势**：
- `__init__` 仍然是唯一的字段定义处，不存在两处维护的问题
- `Annotated` 是 Python 标准库（3.9+），不引入新概念
- 提取器读取 `Annotated` 内的 `FieldInfo` 即可获得 description、约束等全部 Pydantic 元数据
- IDE 对 `__init__` 参数天然有补全支持
- **description 直接出现在 YAML 补全提示中** — Bob 看到 `temperature` 时同时看到约束 `ge=0, le=2`

### 3.4 Tier 3: 外部 Config 类引用 — 完全控制（逃生舱）

对于需要 model_validator、嵌套结构等复杂场景，允许指向一个外部 Pydantic model：

```python
# core/llm/configs/openai_config.py   （或同文件中定义）
class OpenAIConfig(BaseModel):
    model_id: str = Field(description="模型 ID")
    temperature: float = Field(0.0, ge=0, le=2)
    max_tokens: int = Field(4096, gt=0)

    @model_validator(mode="after")
    def validate_max_tokens(self):
        if self.model_id.startswith("o") and self.max_tokens > 16384:
            raise ValueError("o-series max_tokens 上限 16384")
        return self


# core/llm/providers/openai.py
class ChatOpenAI(ChatBaseModel):
    __config_schema__ = OpenAIConfig   # ← 显式引用，提取器优先读此属性

    def __init__(self, config: OpenAIConfig):
        self.model_id = config.model_id
        ...
```

也支持 `__init__` 接收单个 BaseModel 参数时自动识别：

```python
class ChatOpenAI(ChatBaseModel):
    def __init__(self, config: OpenAIConfig):   # ← 单参数 + 类型是 BaseModel 子类
        ...
# 提取器自动识别 OpenAIConfig 作为 config schema
```

**Tier 3 的 schema 由用户完全掌控**：提取器直接返回 `OpenAIConfig`，不检测 `**kwargs`、不覆盖 `model_config`。用户显式声明了完整的 schema，意味着他已经完全掌控了 schema 的行为（包括 `extra` 策略、validator 等）。

### 3.5 对比

| 维度 | Tier 1: 纯 `__init__` | Tier 1.5: + Docstring | Tier 2: + `Annotated` | Tier 3: 外部 Config 类 |
|------|---------------------|----------------------|----------------------|---------------------|
| 额外代码量 | 零 | 零（docstring 通常已有） | 极少（Annotated 包装） | 需独立定义 BaseModel |
| 元数据丰富度 | type + default | + description | + description, 约束, 枚举 | + validator, 嵌套结构 |
| IDE 补全质量 | 基础 | 中（含 description） | 高（含 description + 约束） | 最高 |
| 两处维护风险 | 无 | 无 | 无 | 有（config 类 vs __init__） |
| `**kwargs` 检测 | **是** | **是** | **是** | **否** — 尊重 schema |
| 适用场景 | 快速原型 | 已有 docstring 的代码 | **大多数生产代码** | 复杂校验、嵌套 config |
| Bob 获得的体验 | 字段名 + 类型 | + 描述 | + 描述 + 约束提示 | + 跨字段校验 |

---

## 4. Config 提取器

提取器是整个 config-typing 管线的入口。它从已注册的实现类中提取出 Pydantic BaseModel 形式的 config schema，供后续构建器和代码生成器使用。

### 4.1 提取优先级

```
cls.__config_schema__              存在 → 直接返回（尊重其 model_config，不做额外判断）
       │ 不存在
       ▼
cls.__init__(self, config: T)      T 是 BaseModel 子类 → 返回 T
       │ 不匹配
       ▼
MRO 遍历 → 找到实际定义 __init__ 的类
       │
       ▼
cls.__init__ 签名 + type hints     提取各参数 → 动态创建 Pydantic model
       │
       ├─ 参数有 Annotated[T, Field(...)]  →  保留 FieldInfo 元数据（Tier 2）
       ├─ 参数无 Annotated，docstring 有描述 → type + default + description（Tier 1.5）
       ├─ 参数无 Annotated，无 docstring    →  仅 type + default（Tier 1）
       │
       ├─ 描述来源优先级:
       │   Annotated[T, Field(description=...)] > docstring Args: > 无描述
       │
       └─ **kwargs / *args 检测:
           ├─ 无 → extra="forbid" (closed schema)
           └─ 有 → extra="allow"  (open schema)
```

### 4.2 提取策略详解

#### Tier 3: `__config_schema__` — 显式引用（最高优先级）

- **触发条件**：`cls.__config_schema__` 属性存在
- **校验**：必须是 `BaseModel` 子类，否则抛 `TypeError`
- **行为**：**直接原样返回该 schema**。不检测 `__init__` 的 `**kwargs`，不覆盖 `model_config`
- **理由**：用户显式声明了完整的 BaseModel schema，意味着他已经完全掌控 schema 行为。提取器不应做任何假设或覆盖

#### Tier 3 变体: 单参数 BaseModel 自动识别

- **触发条件**：`__config_schema__` 不存在，`__init__` 除 `self` 外只有一个参数，且该参数的类型是 `BaseModel` 子类
- **行为**：返回该参数的类型（如 `OpenAIConfig`）
- **场景**：`def __init__(self, config: OpenAIConfig)` — 无需显式 `__config_schema__`
- **注意**：如果参数类型被 `Annotated` 包装，先解包再判断

#### Tier 1 / 1.5 / 2: 签名反射（默认路径）

从 `__init__` 的参数签名中提取字段定义，动态创建 Pydantic model。

**MRO 感知参数收集**：
- 如果 `cls` 自身没有定义 `__init__`（即 `'__init__' not in cls.__dict__`），沿 MRO 链找到实际定义 `__init__` 的类
- 使用实际定义者的 `__init__` 进行签名提取，确保继承的参数不会遗漏
- 示例：`class SubAgent(AgentBase)` 没有自己的 `__init__` → 提取 `AgentBase.__init__` 的参数

**参数过滤规则**：
- 排除 `self`
- 排除 `*args`（`VAR_POSITIONAL`）和 `**kwargs`（`VAR_KEYWORD`）— 这些不生成字段，但用于 open/closed 判定
- 剩余参数（`POSITIONAL_ONLY`, `POSITIONAL_OR_KEYWORD`, `KEYWORD_ONLY`）生成字段

**类型提取**：
- 使用 `get_type_hints(cls.__init__, include_extras=True)` 获取完整类型标注
  - `include_extras=True` 保留 `Annotated` 中的元数据
  - 自动处理 `from __future__ import annotations`（PEP 563 延迟求值）

**Annotated 处理**（Tier 2）：
- 检测 `Annotated[T, Field(...)]` 形式的标注
- 从 `Annotated` 的元数据中查找 `FieldInfo` 实例
- 合并 `FieldInfo` 中的元数据（description, ge, le, gt, lt 等）与参数的默认值
- 如果 `Annotated` 中无 `FieldInfo`，退化为 Tier 1.5/1 处理

**Docstring 描述提取**（Tier 1.5）：
- 当参数没有通过 `Annotated[T, Field(description=...)]` 提供描述时，尝试从类的 docstring 中提取
- 支持 Google / NumPy / reST 三种标准 docstring 格式的 `Args:` / `Parameters:` 段落
- 按参数名匹配 docstring 中的描述文本
- **描述来源优先级**：`Annotated[T, Field(description=...)]` > docstring `Args:` > 无描述
- 可选依赖 `docstring_parser` 库；未安装时静默退化为 Tier 1（无描述）

**默认值处理**：
- 参数有默认值 → 字段有默认值
- 参数无默认值 (`inspect.Parameter.empty`) → 字段必填 (`...`)

**Open/Closed Schema 判定**：
- 检查 `__init__` 参数中是否存在 `VAR_POSITIONAL` (`*args`) 或 `VAR_KEYWORD` (`**kwargs`)
- 无 → 动态创建的 model 设置 `extra="forbid"`（closed，严格校验）
- 有 → 动态创建的 model 设置 `extra="allow"`（open，放行额外字段）

**动态 model 命名**：`{ClassName}Config`，如 `ChatOpenAI` → `ChatOpenAIConfig`

### 4.3 提取示例

**Tier 1.5 提取（docstring 描述）**：

```python
# 输入
class ChatOpenAI(ChatBaseModel):
    """OpenAI LLM provider.

    Args:
        model_id: 模型 ID，如 gpt-4o
        temperature: 生成温度，0-2 之间
        max_tokens: 最大输出 token 数
    """

    def __init__(self, *, model_id: str, temperature: float = 0.0,
                 max_tokens: int = 4096):
        ...
```

```python
# 提取器输出等价于
class ChatOpenAIConfig(BaseModel):
    model_config = ConfigDict(extra="forbid")  # ← 无 **kwargs，closed schema

    model_id: str = Field(..., description="模型 ID，如 gpt-4o")      # ← docstring
    temperature: float = Field(0.0, description="生成温度，0-2 之间")   # ← docstring
    max_tokens: int = Field(4096, description="最大输出 token 数")     # ← docstring
```

**MRO 继承提取**：

```python
# 输入 — SubAgent 没有自己的 __init__
class AgentBase(metaclass=AgentRegistrar.Meta):
    __abstract__ = True

    def __init__(self, *, max_steps: int = 100, timeout: int = 300):
        ...

class SubAgent(AgentBase):
    """继承 AgentBase 的所有参数，无需重写 __init__"""
    pass
```

```python
# 提取器沿 MRO 找到 AgentBase.__init__，输出等价于
class SubAgentConfig(BaseModel):
    model_config = ConfigDict(extra="forbid")

    max_steps: int = 100    # ← 来自 AgentBase.__init__
    timeout: int = 300      # ← 来自 AgentBase.__init__
```

**Tier 2 提取（推荐路径）**：

```python
# 输入
class ChatAnthropic(ChatBaseModel):
    def __init__(
        self, *,
        model_id: Annotated[str, Field(description="claude-sonnet-4-20250514")] ,
        temperature: Annotated[float, Field(ge=0, le=1)] = 0.0,
        max_tokens: Annotated[int, Field(gt=0)] = 4096,
        top_k: Annotated[int | None, Field(description="Anthropic 特有")] = None,
        extended_thinking: Annotated[bool, Field(description="启用 extended thinking")] = False,
        thinking_budget: int = 10000,   # ← 无 Annotated，Tier 1 提取
    ):
        ...
```

```python
# 提取器输出等价于
class ChatAnthropicConfig(BaseModel):
    model_config = ConfigDict(extra="forbid")  # ← 无 **kwargs，closed schema

    model_id: str = Field(description="claude-sonnet-4-20250514")
    temperature: float = Field(0.0, ge=0, le=1)
    max_tokens: int = Field(4096, gt=0)
    top_k: int | None = Field(None, description="Anthropic 特有")
    extended_thinking: bool = Field(False, description="启用 extended thinking")
    thinking_budget: int = 10000   # 无 description，纯 type + default
```

**Open Schema 提取**：

```python
# 输入
class FlexibleLLM(ChatBaseModel):
    def __init__(self, *, model_id: str, temperature: float = 0.0, **kwargs):
        ...
```

```python
# 提取器输出等价于
class FlexibleLLMConfig(BaseModel):
    model_config = ConfigDict(extra="allow")  # ← 有 **kwargs，open schema

    model_id: str = ...
    temperature: float = 0.0
```

### 4.4 API 契约

#### `extract_config_schema(cls)`

| 项目 | 描述 |
|------|------|
| **签名** | `extract_config_schema(cls: type) -> type[BaseModel] \| None` |
| **职责** | 从已注册的实现类中提取 config schema。按 Tier 3 → 单参数 BaseModel → Tier 1/2 的优先级尝试 |
| **参数** | `cls` — 已注册的实现类 |
| **返回值** | Pydantic `BaseModel` 子类（含 `model_config` 中的 `extra` 策略），或 `None`（无法提取时） |
| **异常** | `TypeError` — 当 `__config_schema__` 存在但不是 `BaseModel` 子类时 |

**行为要点**：
1. Tier 3 路径直接返回 `__config_schema__`，**不做任何修改或覆盖**
2. 单参数 BaseModel 路径直接返回参数类型，**不做任何修改**
3. 签名反射路径（Tier 1/1.5/2）：
   a. **MRO 遍历**：如果 `cls` 自身没有 `__init__`，沿 MRO 找到实际定义 `__init__` 的类
   b. 提取签名参数，处理 `Annotated` 元数据
   c. **Docstring 描述回退**：对没有 `Field(description=...)` 的参数，从类（或 MRO 中定义 `__init__` 的类）的 docstring 提取描述
   d. 设置 `extra` 策略（基于 `**kwargs` 检测）
   e. 动态创建 Pydantic model
4. 如果 `__init__` 无参数（除 self 外），返回 `None`
5. 如果 `inspect.signature()` 或 `get_type_hints()` 失败，返回 `None`

---

## 5. Config Union 构建器

构建器从单个 registrar 的注册表出发，为每个已注册的 key 提取 config schema，注入 discriminator 字段，最终组装成 Pydantic discriminated union。

### 5.1 设计原则

**每个 `LayerRegistrar` 独立生成自己的 config union。** 包不知道也不关心消费者的顶层 config 结构。

```
layer-registry (PyPI 包)
├── 提供: extract + build union + generate 能力
└── 不提供: 顶层 BenchConfig 这类领域特定的组合

消费者项目 (如 browseruse-bench)
├── 使用 layer-registry 为每个 layer 生成 config
├── 自行组合顶层 config
└── 自行添加领域特定的 validator
```

### 5.2 Registrar 的 discriminator 声明

`create_registrar()` 的 `discriminator_field` 参数声明各 layer 的区分字段：

```python
from layer_registry import create_registrar

AgentRegistrar = create_registrar("agent", AgentProtocol, discriminator_field="name")
LLMRegistrar = create_registrar("llm", ChatModelProtocol, discriminator_field="provider")
ProviderRegistrar = create_registrar("provider", BrowserProviderProtocol, discriminator_field="id")
```

### 5.3 构建流程

```
LLMRegistrar (discriminator_field="provider")
    │
    ├─ ChatOpenAI     → extract → OpenAIConfig (extra="forbid")
    ├─ ChatAnthropic  → extract → AnthropicConfig (extra="forbid")
    ├─ FlexibleLLM    → extract → FlexibleConfig (extra="allow")
    └─ ...
    │
    ▼
build_layer_config(LLMRegistrar)
    │
    ├─ per-key models (注入 discriminator):
    │   OpenaiLLMConfig(provider: Literal["openai"], model_id, ...)      extra="forbid"
    │   AnthropicLLMConfig(provider: Literal["anthropic"], model_id, ...) extra="forbid"
    │   FlexibleLLMConfig(provider: Literal["flexible"], model_id, ...)   extra="allow"
    │
    └─ union type:
        LLMConfig = Annotated[Union[OpenAI..., Anthropic..., ...], Field(discriminator="provider")]
```

**Discriminator 注入规则**：
- 为每个 per-key model 添加 `Literal[key]` 类型的 discriminator 字段，并设置默认值为 key 本身
- 如果原 schema 已有同名字段，用 `Literal[key]` 覆盖（避免重复）
- 注入时**保留原 schema 的 `extra` 策略**（从原 model 的 `model_config` 继承）

**Model 命名规则**：
- `{KeyPart}{LayerPart}Config`
- KeyPart: key 的 snake_case → TitleCase（`"openai"` → `"Openai"`，`"browser_use"` → `"BrowserUse"`）
- LayerPart: layer name ≤ 3 字符用全大写（`"llm"` → `"LLM"`），否则用 Title（`"agent"` → `"Agent"`）
- 示例：`"openai"` + `"llm"` → `"OpenaiLLMConfig"`；`"browser_use"` + `"agent"` → `"BrowserUseAgentConfig"`

**Union 构建规则**：
- 单个 model → 直接使用该 model（不包 Union）
- 多个 model → `Annotated[Union[...], Field(discriminator=...)]`

### 5.4 消费者自行组合顶层 Config

```python
# browseruse_bench/config.py — 消费者代码，非 layer-registry 生成

from generated import LLMConfig, AgentConfig, ProviderConfig
from pydantic import BaseModel

class BenchConfig(BaseModel):
    """browseruse-bench 的完整实验配置。消费者自定义。"""
    llm: LLMConfig                         # ← 由 layer-registry 生成
    agent: AgentConfig                     # ← 由 layer-registry 生成
    provider: ProviderConfig               # ← 由 layer-registry 生成
    benchmark: BenchmarkConfig             # ← 消费者自定义
    timeout: int = 300
```

好处：
- **layer-registry 包保持领域无关** — 不知道什么是 "benchmark"
- **每个 layer 的 config 可独立使用** — 其他项目可能只用 LLMConfig 而不用 AgentConfig
- **消费者完全控制顶层结构** — 可以添加任意领域特定字段和 validator

### 5.5 API 契约

#### `LayerConfigResult`

| 项目 | 描述 |
|------|------|
| **职责** | 单个 layer 的 config 构建结果容器 |
| **属性** | |
| `union_type: type` | 该 layer 的 discriminated union type。单 key 时为 model 本身，多 key 时为 `Annotated[Union[...], Field(discriminator=...)]` |
| `per_key_models: dict[str, type[BaseModel]]` | key → 注入了 discriminator 的 per-key config model |
| `layer_name: str` | Layer 名称（来自 registrar，如 `"llm"`、`"agent"`） |
| `discriminator_field: str` | 区分字段名（如 `"provider"`、`"name"`） |

#### `build_layer_config(registrar)`

| 项目 | 描述 |
|------|------|
| **签名** | `build_layer_config(registrar) -> LayerConfigResult` |
| **职责** | 从单个 registrar 构建该 layer 的 config union。遍历注册表所有 key，提取 schema，注入 discriminator，组装 Union |
| **参数** | `registrar` — `LayerRegistrar` 实例或子类，必须设置了 `discriminator_field` |
| **返回值** | `LayerConfigResult` 包含 union type 和各 per-key model |

**行为要点**：
1. 遍历 `registrar.registry.items()` 中的每一个 `(key, cls)`
2. 对每个 `cls` 调用 `extract_config_schema(cls)` 提取 schema
3. 如果 schema 为 `None` → 创建仅含 discriminator 字段的空 model（`extra="forbid"`）
4. 如果 schema 非 `None` → 注入 discriminator 字段，保留原 schema 的 `extra` 策略
5. 将所有 per-key model 组装成 discriminated union

---

## 6. 代码生成器

代码生成器将 `LayerConfigResult` 序列化为两种输出文件：Python stub（IDE 补全）和 JSON Schema（YAML 补全）。

### 6.1 Python Stub 生成

将 `LayerConfigResult` 序列化为**可读的 Python 源文件**。

**输出结构**：
1. 文件头：auto-generated 声明 + 时间戳 + layer 信息 + 条目数
2. Imports：`from __future__ import annotations`、`typing`、`pydantic`
3. 每个 key 的 config model class：
   - `model_config = ConfigDict(extra="forbid" | "allow")`
   - open schema 的 class 添加 docstring 标注 "此实现接受额外参数"
   - 各字段定义（type + default + Field 元数据）
   - docstring 提取的 description 同样序列化为 `Field(description=...)`
4. Discriminated union type alias
5. **PEP 561 `py.typed` 标记**：在 stub 输出目录生成空的 `py.typed` 文件，让 mypy/pyright 自动发现生成的类型信息

**类型到源码的转换规则**：
- `Literal["openai"]` → `Literal["openai"]`
- `Union[str, None]` → `str | None`（Python 3.10+ 简写）
- 其他 Union → `X | Y | Z`
- 基础类型 → 类名（`str`, `int`, `float`, `bool`）

**Field 元数据的序列化规则**：
- `description` 非空 → 添加 `description=...`
- `metadata` 中的约束（`ge`, `le`, `gt`, `lt`）→ 逐个添加
- 有任何元数据 → 使用 `Field(default, ...)` 格式
- 无元数据 → 直接 `= default`

### 6.2 JSON Schema 生成

将 `LayerConfigResult` 的 union type 通过 Pydantic 的 `TypeAdapter` 序列化为 JSON Schema。

**行为要点**：
- 使用 `TypeAdapter(result.union_type).json_schema()` 生成标准 JSON Schema
- `extra="forbid"` 的 model → JSON Schema 中 `"additionalProperties": false`（YAML Language Server 据此红线未知字段）
- `extra="allow"` 的 model → 无 `additionalProperties` 限制
- 额外添加 `"x-discriminator"` 字段，值为 discriminator field name

### 6.3 生成输出示例

每个 layer 独立一个文件。以 LLM layer 为例：

```python
"""Auto-generated by layer-registry. DO NOT EDIT.

Generated at: 2026-03-11T10:00:00Z
Layer: llm
Discriminator: provider
Entries: 6
"""

from __future__ import annotations

from typing import Annotated, Literal, Union
from pydantic import BaseModel, ConfigDict, Field


class OpenaiLLMConfig(BaseModel):
    model_config = ConfigDict(extra="forbid")

    provider: Literal["openai"] = "openai"
    model_id: str = Field(..., description="模型 ID，如 gpt-4o")
    temperature: float = Field(0.0, ge=0, le=2)
    max_tokens: int = Field(4096, gt=0)
    top_p: float = Field(1.0, ge=0, le=1)
    base_url: str | None = Field(None, description="自定义 API endpoint")


class AnthropicLLMConfig(BaseModel):
    model_config = ConfigDict(extra="forbid")

    provider: Literal["anthropic"] = "anthropic"
    model_id: str = Field(..., description="claude-sonnet-4-20250514")
    temperature: float = Field(0.0, ge=0, le=1)
    max_tokens: int = Field(4096, gt=0)
    top_k: int | None = Field(None, description="Anthropic 特有")
    extended_thinking: bool = Field(False, description="启用 extended thinking")
    thinking_budget: int = 10000


class FlexibleLLMConfig(BaseModel):
    """此实现接受额外参数 (**kwargs)。未声明的字段不会报错。"""
    model_config = ConfigDict(extra="allow")

    provider: Literal["flexible"] = "flexible"
    model_id: str = ...
    temperature: float = 0.0


LlmConfig = Annotated[
    Union[
        OpenaiLLMConfig,
        AnthropicLLMConfig,
        FlexibleLLMConfig,
    ],
    Field(discriminator="provider"),
]
```

### 6.4 API 契约

#### `generate_layer_config_source(result)`

| 项目 | 描述 |
|------|------|
| **签名** | `generate_layer_config_source(result: LayerConfigResult) -> str` |
| **职责** | 将单个 layer 的 config 构建结果序列化为 Python 源文件字符串 |
| **参数** | `result` — `build_layer_config()` 返回的 `LayerConfigResult` |
| **返回值** | 完整的 Python 源文件内容（字符串），包含文件头、imports、model classes、union type alias |

#### `generate_layer_json_schema(result)`

| 项目 | 描述 |
|------|------|
| **签名** | `generate_layer_json_schema(result: LayerConfigResult) -> dict` |
| **职责** | 将单个 layer 的 config 构建结果序列化为 JSON Schema dict（用于 YAML 编辑器补全） |
| **参数** | `result` — `build_layer_config()` 返回的 `LayerConfigResult` |
| **返回值** | JSON Schema dict，包含 `"x-discriminator"` 扩展字段 |

---

## 7. 自动保鲜机制

### 7.1 Stubs vs Runtime Validation

| 关注点 | 载体 | 目的 | 更新时机 | 来源 |
|--------|------|------|---------|------|
| **Stubs** | 磁盘文件 (`.py` + `.schema.json`) | IDE 补全 | discover() 时按需更新 | 上一次 discover() 的快照 |
| **Runtime validation** | 内存中的 union type | fail-fast 校验 | load_config() 时实时构建 | 当前运行的注册表 |

**Runtime validation 不依赖 stub 文件**。它直接从当前注册表调用 `build_layer_config()` 构建 union type。即使 stubs 过期，runtime 校验仍然准确。

### 7.2 注册表指纹

指纹用于判断注册表是否发生变化。包含以下信息的哈希：
- 所有已注册的 key（排序后）
- 每个 key 对应类的 `__qualname__` 和 `__module__`
- 每个类的 `__init__` 签名字符串（沿 MRO 找到实际定义 `__init__` 的类）
- 每个类的 docstring（用于 Tier 1.5 描述变化检测）

**任何以下变化都会导致指纹不同**：
- 新增或删除了 key
- key 对应的类发生了替换（不同的 qualname 或 module）
- 类的 `__init__` 参数发生了变化（增删参数、改类型、改默认值）
- 类的 docstring 发生了变化（影响 Tier 1.5 描述提取）

**指纹存储位置**：生成文件目录下的 `.registry_fingerprint` 文件，JSON 格式：
```json
{
  "llm": "a1b2c3d4e5f6g7h8",
  "agent": "h8g7f6e5d4c3b2a1"
}
```

### 7.3 discover() 集成

`discover()` 新增可选的自动保鲜逻辑。在原有的 "递归 import → 填充注册表" 之后：

1. 读取项目根目录的 `layer-registry.yaml` 配置文件
2. 如果配置文件不存在 → 跳过自动更新
3. 对配置文件中每个 layer：
   a. 计算当前注册表指纹
   b. 与缓存指纹比较
   c. 相同 → 跳过
   d. 不同 → 重新生成 Python stub 和 JSON Schema（如配置了）→ 更新缓存指纹

**`discover()` 新增参数**：`auto_update_stubs: bool = True`，允许消费者禁用自动更新。CLI 的手动命令调用时传 `False`。

### 7.4 API 契约

#### `compute_registry_fingerprint(registrar)`

| 项目 | 描述 |
|------|------|
| **签名** | `compute_registry_fingerprint(registrar) -> str` |
| **职责** | 计算 registrar 当前注册表的指纹哈希 |
| **参数** | `registrar` — `LayerRegistrar` 实例或子类 |
| **返回值** | 指纹哈希字符串（如 SHA-256 的前 16 位十六进制） |

#### `load_cached_fingerprint(fingerprint_path, layer_name)`

| 项目 | 描述 |
|------|------|
| **签名** | `load_cached_fingerprint(fingerprint_path: Path, layer_name: str) -> str \| None` |
| **职责** | 从缓存文件加载指定 layer 的指纹 |
| **返回值** | 缓存的指纹字符串，或 `None`（文件不存在/解析失败/无此 layer） |

#### `save_fingerprint(fingerprint_path, layer_name, fingerprint)`

| 项目 | 描述 |
|------|------|
| **签名** | `save_fingerprint(fingerprint_path: Path, layer_name: str, fingerprint: str) -> None` |
| **职责** | 保存指定 layer 的指纹到缓存文件。如果文件已存在则更新对应 layer 的条目 |

---

## 8. 与注册机制的集成

### 8.1 LayerRegistrar 新增 API

#### `build_config()`

| 项目 | 描述 |
|------|------|
| **签名** | `classmethod build_config(cls) -> LayerConfigResult` |
| **职责** | 构建该 layer 的 config union 类型。委托给 `build_layer_config(cls)` |
| **前置条件** | `cls.discriminator_field` 必须已设置，否则抛 `ValueError` |
| **返回值** | `LayerConfigResult` |

#### `config_union_type()`

| 项目 | 描述 |
|------|------|
| **签名** | `classmethod config_union_type(cls) -> type` |
| **职责** | 快捷方法，直接获取 union type。用于 `load_config()` 中的 runtime validation |
| **行为** | 等价于 `cls.build_config().union_type` |
| **重要** | 从当前注册表实时构建，**不依赖 stub 文件** |

#### `get_config_schema(key)`

| 项目 | 描述 |
|------|------|
| **签名** | `classmethod get_config_schema(cls, key: str) -> type[BaseModel] \| None` |
| **职责** | 获取指定 key 的 config schema（调用 `extract_config_schema`） |
| **异常** | 如果 key 不在注册表中，抛 `KeyError` |

### 8.2 AutoRegistrar `__config_schema__` 校验

在 `AutoRegistrar.__new__()` 中增加注册时校验：

- 如果子类的 `namespace` 中存在 `__config_schema__` 属性
- 校验其必须是 `type` 且是 `BaseModel` 子类
- 不满足 → 抛 `TypeError`（fail-fast，不允许注册一个 schema 定义不合法的类）

### 8.3 Runtime Validation 流程

消费者在程序入口调用 `load_config()` 时，应使用 `config_union_type()` 从注册表实时构建 union type，而非依赖 stub 文件：

```
load_config("experiment.yaml")
    │
    ├─ 读取 YAML → raw dict
    ├─ 用 registrar.config_union_type() 获取实时 union type
    ├─ Pydantic model_validate(raw)
    │   ├─ discriminator 匹配 → 选择正确的 per-key model
    │   ├─ extra="forbid" + 未知字段 → ValidationError
    │   ├─ extra="allow" + 未知字段 → 放行
    │   └─ 类型/约束不满足 → ValidationError
    │
    ├─ 校验通过 → 返回 typed config
    └─ 校验失败 → 立即终止，清晰错误信息
                   (不进入任何业务逻辑)
```

---

## 9. CLI 工具

### 9.1 命令设计

```bash
# 为单个 layer 生成 config 类型文件
layer-registry generate-config \
  --registrar "core.llm._registrar:LLMRegistrar" \
  --discover "core.llm.providers" \
  --output "generated/llm_config.py" \
  --json-schema "generated/llm_config.schema.json"

# 为多个 layer 批量生成
layer-registry generate-config \
  --config layer-registry.yaml

# 手动强制更新所有 stubs（忽略指纹缓存）
layer-registry update-stubs \
  --config layer-registry.yaml

# 列出某个 registrar 的所有注册项及其 config schema
layer-registry inspect \
  --registrar "core.llm._registrar:LLMRegistrar" \
  --discover "core.llm.providers"
```

#### `generate-config` 命令

| 项目 | 描述 |
|------|------|
| **职责** | 从注册表生成 typed config 文件（Python stub 和/或 JSON Schema） |
| **模式 1** | 单 layer：通过 `--registrar`, `--discover`, `--output`, `--json-schema` 参数指定 |
| **模式 2** | 批量：通过 `--config` 指定 YAML 配置文件 |
| **行为** | 触发 `discover()`（`auto_update_stubs=False`）→ 加载 registrar → `build_layer_config()` → 序列化输出 |
| **指纹** | 生成后更新缓存指纹 |

#### `update-stubs` 命令

| 项目 | 描述 |
|------|------|
| **职责** | 手动强制更新所有 stubs，**忽略指纹缓存** |
| **参数** | `--config` — 批量配置 YAML 文件（必填） |
| **场景** | 框架开发者在发布前确保 stubs 最新；用户怀疑 stubs 过期时手动刷新 |
| **行为** | 对配置文件中每个 layer 无条件重新生成 → 更新指纹缓存 |

#### `inspect` 命令

| 项目 | 描述 |
|------|------|
| **职责** | 列出 registrar 的所有注册项及其 config schema（调试/查看用） |
| **输出** | 每个 key：key name、类名、config 字段列表（名称 + 类型 + 默认值 + 是否 open schema） |

### 9.2 批量配置文件

```yaml
# layer-registry.yaml

discover:
  - agents.browser_use
  - agents.skyvern
  - agents.agent_tars
  - core.llm.providers
  - core.providers
  - evaluator.evaluators
  - benchmark_data
  - core.dom.filters

output_dir: generated   # stubs 输出目录，也存放 .registry_fingerprint

layers:
  - registrar: agents._registrar:AgentRegistrar
    output: generated/agent_config.py
    json_schema: generated/agent_config.schema.json

  - registrar: core.llm._registrar:LLMRegistrar
    output: generated/llm_config.py
    json_schema: generated/llm_config.schema.json

  - registrar: core.providers._registrar:ProviderRegistrar
    output: generated/provider_config.py
    json_schema: generated/provider_config.schema.json

  - registrar: evaluator._registrar:EvaluatorRegistrar
    output: generated/evaluator_config.py

  - registrar: core.dom.filters._registrar:DOMFilterRegistrar
    output: generated/dom_filter_config.py
```

---

## 10. 包结构与公共 API

### 10.1 包结构

```
layer-registry/
├── pyproject.toml
├── layer_registry/
│   ├── __init__.py                # 统一导出 registration + config 的公共 API
│   │
│   ├── registration/              # 自动注册子包
│   │   ├── __init__.py
│   │   ├── registry.py            # LayerRegistry[P]
│   │   ├── key_transform.py       # KeyTransform 协议 + 内建实现
│   │   ├── auto.py                # create_auto_registrar()
│   │   ├── registrar.py           # LayerRegistrar[P] + create_registrar()
│   │   └── discover.py            # discover() + 自动保鲜集成
│   │
│   ├── config/                    # Config typing 生成子包
│   │   ├── __init__.py            # 导出 config API
│   │   ├── extractor.py           # extract_config_schema() — 含 MRO 遍历 + docstring 提取
│   │   ├── docstring.py           # docstring 参数描述解析（可选依赖 docstring_parser）
│   │   ├── builder.py             # build_layer_config(), LayerConfigResult
│   │   ├── codegen.py             # generate_layer_config_source() — 含 py.typed 生成
│   │   ├── json_schema.py         # generate_layer_json_schema()
│   │   └── fingerprint.py         # 注册表指纹计算与缓存
│   │
│   └── cli.py                     # CLI 入口
│
└── tests/
    ├── test_registry.py
    ├── test_registrar.py
    ├── test_extractor.py
    ├── test_builder.py
    ├── test_codegen.py
    └── test_fingerprint.py
```

### 10.2 公共 API 导出

**`layer_registry/__init__.py`** — 统一导出 registration API：

| 导出 | 来源 | 用途 |
|------|------|------|
| `create_registrar` | `registration.registrar` | **推荐入口** — 一行创建完整的 registrar |
| `LayerRegistry` | `registration.registry` | 底层注册表（高级场景） |
| `LayerRegistrar` | `registration.registrar` | 底层 registrar 基类（高级场景） |
| `create_auto_registrar` | `registration.auto` | 底层 metaclass 工厂（高级场景） |
| `discover` | `registration.discover` | 模块发现 + 自动保鲜 |
| `infer_key` | `registration.key_transform` | Key 推断工具 |

**`layer_registry/config/__init__.py`** — 导出 config API：

| 导出 | 来源 | 用途 |
|------|------|------|
| `extract_config_schema` | `config.extractor` | 从实现类提取 config schema（含 MRO 遍历 + docstring 回退） |
| `build_layer_config` | `config.builder` | 构建 layer 的 config union |
| `LayerConfigResult` | `config.builder` | 构建结果容器 |
| `generate_layer_config_source` | `config.codegen` | 生成 Python stub 源码 |
| `generate_layer_json_schema` | `config.json_schema` | 生成 JSON Schema |
| `compute_registry_fingerprint` | `config.fingerprint` | 计算注册表指纹 |

### 10.3 消费者使用最小路径

**框架开发者（一次性设置）**：

```python
# Step 1: 创建 Registrar（一行）
from layer_registry import create_registrar
LLMRegistrar = create_registrar("llm", ChatModelProtocol, discriminator_field="provider")

# Step 2: 定义基类
class ChatBaseModel(metaclass=LLMRegistrar.Meta):
    __abstract__ = True

# Step 3: 生成 stubs（CLI 或代码）
# layer-registry generate-config --config layer-registry.yaml

# Step 4: commit generated/ 到仓库
```

**配置使用者（Bob 的日常）**：

```yaml
# 写 config.yaml → IDE 补全 → 运行 → fail-fast 校验
llm:
  provider: openai
  model_id: gpt-4o
  temperature: 0.5
```

---

## 11. 完整数据流

```
实现者写实现类                    自动保鲜                     Bob 使用
──────────────                ──────────                  ──────────

class ChatOpenAI(ChatBase):
    def __init__(
        self, *,
        model_id: Annotated[
          str, Field(desc="..")
        ],
        temperature: ... = 0.0,   无 **kwargs → closed schema
    ):
        │
        │ import-time
        ▼
  AutoRegistrar.__new__()
        │
  registry["openai"] = ChatOpenAI
        │
        │
        ▼
  discover("my_app.llm")
        │
        ├─ 填充注册表
        │
        ├─ 计算指纹
        │   │
        │   ├─ == 缓存 → 跳过
        │   └─ != 缓存 → 自动重新生成
        │       │
        │       ├─ 提取器
        │       │   ├─ Tier 3: __config_schema__ (尊重原 schema)
        │       │   ├─ 单参数 BaseModel (自动识别)
        │       │   ├─ MRO 遍历 → 找到实际定义 __init__ 的类
        │       │   ├─ Tier 2: Annotated (+ metadata)
        │       │   ├─ Tier 1.5: docstring Args: → description 回退
        │       │   ├─ Tier 1: __init__ sig (type + default)
        │       │   └─ **kwargs → extra 策略 (仅 Tier 1/1.5/2)
        │       │
        │       ├─ 构建器
        │       │   ├─ inject discriminator
        │       │   ├─ propagate extra policy
        │       │   └─ build Union
        │       │
        │       └─ 代码生成
        │           ├─ llm_config.py ────────── Python IDE 补全
        │           ├─ llm_config.schema.json ─ YAML IDE 补全
        │           ├─ py.typed ─────────────── PEP 561 类型标记
        │           └─ .registry_fingerprint ── 指纹缓存
        │
        ▼
  load_config("experiment.yaml")
        │
        ├─ 从注册表构建 union type（实时，非 stub）
        ├─ Pydantic 全量校验
        │   ├─ discriminator 匹配 → 选择正确的 config model
        │   ├─ extra="forbid" + 未知字段 → ConfigurationError
        │   ├─ extra="allow" + 未知字段 → 放行到 **kwargs
        │   └─ 类型/约束不满足 → ConfigurationError
        │
        ├─ 校验通过 → 程序继续
        └─ 校验失败 → 立即终止，清晰错误信息
                       (不进入任何业务逻辑)
```

---

## 12. Capability 声明（可选扩展）

实现类可通过 `__capabilities__` 声明能力，生成器可提取为能力矩阵：

```python
class LexmountProvider(BrowserProvider):
    __capabilities__ = frozenset(["context", "fingerprint"])

    def __init__(self, *, browser_mode: str = "normal", ...):
        ...

class AgentBayProvider(BrowserProvider):
    __capabilities__ = frozenset(["dom_extraction", "action"])

    def __init__(self, *, image_id: str = "browser_latest", ...):
        ...
```

#### `extract_capabilities(cls)`

| 项目 | 描述 |
|------|------|
| **签名** | `extract_capabilities(cls: type) -> frozenset[str] \| None` |
| **职责** | 从实现类中提取 `__capabilities__` 属性 |
| **返回值** | 能力集合的 `frozenset`，或 `None`（无 `__capabilities__` 时） |

比 v1 的嵌套 `Capabilities` 类更简洁，且能力声明与 config 声明分离。

---

## 13. 设计决策总结

| # | 决策 | 理由 |
|---|------|------|
| 1 | **`__init__` 签名即配置契约** | 单一来源，不需要两处维护；最 Pythonic；实现者零额外代码 |
| 2 | **fail-fast 校验是核心价值** | 解决 mmcv/OpenMMLab 类框架 "跑了 N 步才发现配置错" 的痛点 |
| 3 | **`**kwargs` 检测 → open/closed schema** | 无 `**kwargs` = `extra="forbid"`（严格校验）；有 `**kwargs` = `extra="allow"`（放行额外字段）|
| 4 | **Tier 3 尊重用户的 schema，不做覆盖** | 用户显式声明 `__config_schema__` = 完全掌控 schema 行为，提取器不应假设或覆盖 `extra` 策略 |
| 5 | **Stubs 与 runtime validation 分离** | Stubs 服务 IDE 补全，runtime 从注册表实时构建。即使 stubs 过期，校验仍准确 |
| 6 | **discover() 驱动的自动保鲜** | 注册表指纹变化时自动重新生成 stubs；用户无需手动操作 |
| 7 | **生成文件 commit 到仓库** | 开发者生成后 commit；用户 pip install 即有补全；仅与 Python 版本相关 |
| 8 | **两种输出都是必要的** | Python stub 服务进阶脚本路径；JSON Schema 服务 YAML 日常路径 |
| 9 | **`Annotated[T, Field()]` 携带元数据** | 标准库机制，零新概念引入；Pydantic 原生支持；description 直达 YAML 补全提示 |
| 10 | **`__config_schema__` 作为逃生舱** | 复杂场景（validator、嵌套）需要完整 BaseModel 的能力 |
| 11 | **消灭嵌套 `ConfigSchema` 类** | v1 最大痛点；违反 "实现者零负担" 底线 |
| 12 | **每个 layer 独立生成 config** | 包保持领域无关；layer 间 config 可独立使用 |
| 13 | **消费者自行组合顶层 config** | layer-registry 不知道也不关心消费者的领域结构 |
| 14 | **`discriminator_field` 由 Registrar 声明** | 与 registry 机制内聚；消费者定义 registrar 时一次性指定 |
| 15 | **CLI: generate-config + update-stubs 双命令** | generate-config 按需生成；update-stubs 强制刷新（框架发布前用） |
| 16 | **Bob（配置使用者）的体验是第一优先** | 核心用户不读源码；零知识负担的配置编写是设计的北极星 |
| 17 | **Pydantic discriminated union** | Pydantic v2 原生支持，IDE 类型收窄自动生效 |
| 18 | **JSON Schema 中 `additionalProperties`** | `extra="forbid"` → `additionalProperties: false`，YAML Language Server 据此红线未知字段 |
| 19 | **`__capabilities__` 用 frozenset 而非嵌套类** | 与消灭嵌套类的理念一致；声明更简洁 |
| 20 | **文档描述行为契约而非实现细节** | 作为 plan mode 参考文档，避免限制实现 agent 的自主技术选择 |
| 21 | **Docstring 作为 description 的 fallback 来源** | 大幅缩小 Tier 1 和 Tier 2 的体验差距；实现者只需写常规 docstring（大部分人已经在写），Bob 即可看到参数描述。优先级：`Annotated[T, Field(description=...)]` > docstring > 无描述 |
| 22 | **MRO 感知参数收集** | 子类未重写 `__init__` 时，沿 MRO 链找到实际定义者提取参数。与 hydra-zen (`populate_full_signature`) 和 jsonargparse 对齐，避免继承参数遗漏 |
| 23 | **PEP 561 `py.typed` 标记** | 在生成的 stub 目录中包含空的 `py.typed` 文件，让 mypy/pyright 自动发现生成的类型信息。来源：yacs-stubgen 验证的成熟模式 |
| 24 | **`docstring_parser` 作为可选依赖** | docstring 描述提取是增强而非核心功能；未安装时静默退化为 Tier 1（无描述），不影响 fail-fast 校验 |
