# gitlink-stale — 白名单豁免规则

> 本文档详述"哪些 Issue/PR 永不被 stale skill 处理"的完整规则。
> 豁免规则在 Stage B 执行，先于 AI 判断（节省 API 调用）。

## 1. 豁免原则

**核心思想**：宁可放过，不可误关。

任何满足"长期重要"或"主动声明保留"信号的 Issue 都应被豁免。

| 原则 | 说明 |
|------|------|
| **保守** | 不确定时，豁免（不处理） |
| **多信号** | 任一豁免信号触发即可 |
| **可追溯** | 豁免原因必须记录在报告中 |
| **可配置** | 用户可自定义豁免规则 |

---

## 2. 豁免信号全集

### 2.1 标签豁免（最强信号）

| 标签名（中/英） | 豁免原因 | 默认权重 |
|---------------|---------|---------|
| `pinned` / `置顶` | 显式标记永久保留 | 必豁免 |
| `security` / `安全` | 安全相关，永不自动关闭 | 必豁免 |
| `roadmap` / `路线图` | 长期规划 | 必豁免 |
| `epic` / `里程碑` | 大型任务父节点 | 必豁免 |
| `keep-open` / `保留` | 显式声明 | 必豁免 |
| `in-progress` / `进行中` | 正在处理 | 必豁免 |
| `under-review` / `审查中` | 等待审查 | 必豁免 |
| `help-wanted` | 等社区认领 | 必豁免 |
| `good-first-issue` | 等新手认领 | 必豁免 |
| `p0` / `p1` | 高优先级 | 必豁免 |

### 2.2 Tracker 类型豁免

GitLink 的 tracker_id 映射（参考 issue-triage skill）：

| tracker_id | 名称 | 默认豁免 |
|-----------|------|---------|
| 1 | bug | ❌ 不豁免（仍可能 stale） |
| 2 | feature | ❌ 不豁免 |
| 3 | support | ❌ 不豁免（最易 stale） |
| 4 | doc | ❌ 不豁免 |
| 5 | test | ❌ 不豁免 |
| 6 | duplicate | ✅ 直接关闭（特殊处理） |
| 7 | question | ❌ 不豁免 |
| 自定义 | roadmap | ✅ 豁免 |
| 自定义 | epic | ✅ 豁免 |

### 2.3 优先级豁免

| priority_id | 等级 | 默认豁免 |
|------------|------|---------|
| 1 | low | ❌ 不豁免 |
| 2 | normal | ❌ 不豁免 |
| 3 | high | ⚠️ 仅在 confidence >= 0.9 时处理 |
| 4 | urgent | ✅ 豁免 |

### 2.4 标题模式豁免

匹配以下正则的标题豁免：

```yaml
keep_open_title_patterns:
  - "\\[WIP\\]"
  - "\\[Pinned\\]"
  - "\\[Keep.?Open\\]"
  - "\\[RFC\\]"
  - "^Roadmap:"
  - "^路线图"
  - "^讨论"
  - "^提案"
  - "长期"
  - "permanent"
```

匹配以下模式的标题**强制关闭**（反向豁免）：

```yaml
force_close_title_patterns:
  - "^测试$"           # 仅"测试"两字
  - "^test$"           # 仅"test"
  - "\\[占位\\]"
  - "\\[已过期\\]"
  - "^ignore$"
  - "deprecated"
```

### 2.5 作者豁免

| 作者类型 | 豁免规则 |
|---------|---------|
| 仓库 owner | ✅ 豁免（信任 owner 会跟进） |
| 仓库 member | ✅ 豁免 |
| 资深贡献者（≥ 10 PR merged） | ⚠️ confidence >= 0.85 才处理 |
| 普通用户 | ❌ 不豁免 |
| 路过用户（仅 1 Issue） | ❌ 不豁免（更倾向清理） |

### 2.6 时间豁免

| 时间条件 | 豁免规则 |
|---------|---------|
| 创建时间 < 7 天 | ✅ 豁免（给新 Issue 缓冲期） |
| 最后活动 < 60 天 | ✅ 豁免（未达 stale 阈值） |
| 已 milestone 锁定 | ✅ 豁免 |

### 2.7 关联豁免

| 关联条件 | 豁免规则 |
|---------|---------|
| 有 linked PR（标题含"fixed in #N"） | ✅ 豁免（等 PR 合并） |
| 有子任务（被 epic 引用） | ✅ 豁免 |
| duplicate of 已 closed | 直接关闭（特殊处理） |

---

## 3. 豁免执行算法

```python
def check_exempt(issue, repo_meta, user_meta):
    """
    返回 (is_exempt, reason) 或 (False, None)
    
    顺序：从最强信号到弱信号，任一触发即返回
    """
    
    # 1. 标签豁免（最强）
    EXEMPT_LABELS = {
        "pinned", "置顶",
        "security", "安全",
        "roadmap", "路线图",
        "epic", "里程碑",
        "keep-open", "保留",
        "in-progress", "进行中",
        "under-review", "审查中",
        "help-wanted",
        "good-first-issue",
        "p0", "p1",
    }
    
    for label in get_labels(issue):
        if label.lower() in EXEMPT_LABELS:
            return True, f"含豁免标签: {label}"
    
    # 2. 标题强信号
    import re
    for pattern in KEEP_OPEN_TITLE_PATTERNS:
        if re.search(pattern, issue.subject, re.IGNORECASE):
            return True, f"标题匹配保留模式: {pattern}"
    
    # 3. 优先级豁免
    if issue.priority_id == 4:  # urgent
        return True, "urgent 优先级"
    
    # 4. tracker 豁免
    if issue.tracker_id in [ROADMAP, EPIC]:
        return True, "tracker 是 roadmap/epic"
    
    # 5. 作者豁免
    if issue.author.login == repo_meta.owner:
        return True, "作者是仓库 owner"
    if issue.author.login in repo_meta.members:
        return True, "作者是仓库 member"
    
    # 6. 时间豁免（创建 < 7 天）
    days_since_created = (now() - parse(issue.created_at)).days
    if days_since_created < 7:
        return True, f"创建仅 {days_since_created} 天，在缓冲期内"
    
    # 7. 高优先级的特殊处理
    if issue.priority_id == 3:  # high
        # 不直接豁免，但需要 confidence >= 0.9
        return False, None  # 走正常流程
    
    return False, None


def check_force_close(issue):
    """
    反向豁免：强制关闭
    """
    import re
    for pattern in FORCE_CLOSE_TITLE_PATTERNS:
        if re.search(pattern, issue.subject, re.IGNORECASE):
            return True, f"标题匹配强制关闭模式: {pattern}"
    return False, None
```

---

## 4. 豁免决策流程图

```
       ┌─────────────────────────────┐
       │ Issue 候选（已通过时间过滤）   │
       └────────────┬────────────────┘
                    ▼
       ┌─────────────────────────────┐
       │ 1. 强制关闭模式匹配？         │─── 是 ──→ force_close
       └────────────┬────────────────┘
                    │ 否
                    ▼
       ┌─────────────────────────────┐
       │ 2. 含豁免标签？              │─── 是 ──→ exempt
       └────────────┬────────────────┘
                    │ 否
                    ▼
       ┌─────────────────────────────┐
       │ 3. 标题匹配保留模式？         │─── 是 ──→ exempt
       └────────────┬────────────────┘
                    │ 否
                    ▼
       ┌─────────────────────────────┐
       │ 4. priority == urgent？      │─── 是 ──→ exempt
       └────────────┬────────────────┘
                    │ 否
                    ▼
       ┌─────────────────────────────┐
       │ 5. tracker == roadmap/epic？ │─── 是 ──→ exempt
       └────────────┬────────────────┘
                    │ 否
                    ▼
       ┌─────────────────────────────┐
       │ 6. 作者是 owner/member？     │─── 是 ──→ exempt
       └────────────┬────────────────┘
                    │ 否
                    ▼
       ┌─────────────────────────────┐
       │ 7. 创建 < 7 天？             │─── 是 ──→ exempt
       └────────────┬────────────────┘
                    │ 否
                    ▼
              进入 AI 判断
```

---

## 5. 自定义豁免规则

用户可在仓库根目录创建 `.gitlink-stale.yml` 自定义：

```yaml
# .gitlink-stale.yml
version: 1.0

# 阈值
thresholds:
  stale_days: 60
  close_days: 74
  grace_days: 14

# 豁免标签（追加到默认列表）
exempt_labels:
  - "客户合同"
  - "VIP 用户反馈"

# 豁免标题模式（追加）
exempt_title_patterns:
  - "^\\[长期讨论\\]"

# 强制关闭模式（追加）
force_close_title_patterns:
  - "^spam"

# 豁免用户
exempt_authors:
  - "trusted-contributor"

# 自定义 priority 豁免
exempt_priorities:
  - 4  # urgent
  - 3  # high（比默认更严格）

# 自定义 tracker 豁免
exempt_trackers:
  - 8  # 自定义的"内部任务"
```

> 💡 Skill 在执行前自动加载此文件（如果存在），与默认规则合并。详见 [SKILL.md §4.3](../SKILL.md)。

---

## 6. 豁免审计

报告中必须列出所有被豁免的 Issue，便于人工复核：

```json
{
  "summary": {
    "exempt": 15,
    "exempt_breakdown": {
      "label_pinned": 3,
      "label_security": 2,
      "label_roadmap": 5,
      "priority_urgent": 2,
      "author_owner": 2,
      "title_pattern": 1
    }
  },
  "exempt_items": [
    {
      "number": 88,
      "title": "[Pinned] 项目长期路线图",
      "exempt_reason": "含豁免标签: pinned",
      "exempt_signal": "label_pinned"
    },
    {
      "number": 92,
      "title": "线上数据库故障",
      "exempt_reason": "urgent 优先级",
      "exempt_signal": "priority_urgent"
    }
  ]
}
```

---

## 7. 边界情况

| 情况 | 处理 |
|------|------|
| 同一 Issue 含豁免标签和强制关闭模式 | 豁免优先（保守原则） |
| 标签名大小写不同（`Pinned` vs `pinned`） | 大小写不敏感 |
| 标签名含空格（`keep open`） | 标准化（去空格、转小写）后比较 |
| 标签是 emoji（📌） | 当前不支持，建议搭配文字标签 |
| 作者 ID 已注销（`login == null`） | 不豁免（可能就是僵尸） |
| 用户自定义规则与默认冲突 | 用户规则优先（追加而非覆盖） |

---

## 8. 推荐的标签配置

为了让 Skill 发挥最佳效果，**强烈推荐**仓库具备以下标签：

| 标签名 | 用途 |
|-------|------|
| `pinned` | 显式标记永久保留的 Issue |
| `security` | 安全相关 |
| `roadmap` | 路线图 |
| `stale` | 已被本 Skill 标记 |
| `duplicate` | 重复 Issue |
| `wontfix` | 决定不修复（但保留记录） |

如果仓库缺少这些标签，Skill 在执行前会提示用户创建（不会自动创建，避免越权）。
