# gitlink-stale — AI 判断规则详解

> 本文档说明 Stage C "AI 真假僵尸判断" 的完整信号集与决策算法。
> 这是本 Skill 与简单时间过滤工具的核心差异。

## 1. 为什么需要 AI 判断？

简单按"60 天未活动"一刀切会有大量误判：

| 误判场景 | 简单规则的错误 | AI 判断的纠正 |
|---------|--------------|-------------|
| 路线图 Issue | 标记 stale → 关闭 | 识别为 roadmap，跳过 |
| 等维护者 busy | 标记 stale，作者无感 | 看评论历史，知道在等 |
| 已知 issue 占位 | 标记 stale | 看到维护者说"已知问题，待 v2" |
| 高质量 bug，等修复 | 标记 stale，作者失望 | 看到讨论活跃，跳过 |
| 路过用户的占位 | 一直占着 | 看到作者 0 历史，应清理 |

**核心思想**：`updated_at` 时间 + AI 判断 = 准确识别"真僵尸"。

---

## 2. 判断信号全集

### 2.1 评论历史信号（最重要）

通过 `issue +view --number N` 拿到的 `journals` 数组：

| 信号 | 真僵尸（应处理） | 活跃（应跳过） |
|------|----------------|---------------|
| 最后评论者 | 用户 / 无人 | 维护者 |
| 维护者最后回复时间 | 60+ 天前 | 30 天内 |
| 评论数 | 0-1 条 | ≥ 5 条 |
| 评论内容关键词 | "已知问题"、"占位"、"无复现" | "正在处理"、"待 v2"、"等待上游" |
| 用户最后追问 | 60 天前追问无回复 | 最近有讨论 |

**判断伪代码**：

```python
def analyze_journals(journals, maintainers):
    if not journals:
        return {"truly_stale": True, "score": 0.9, "reason": "0 评论，长期无人理"}
    
    last_journal = journals[-1]
    last_user = last_journal["user"]["login"]
    last_time = parse(last_journal["created_at"])
    
    # 维护者最近回复过 → 跳过
    if last_user in maintainers:
        days_since = (now() - last_time).days
        if days_since < 30:
            return {"truly_stale": False, "score": 0.85,
                    "reason": f"维护者 {last_user} {days_since} 天前回复过"}
    
    # 用户最后回复但维护者没回应 → 真僵尸
    if last_user == issue_author:
        maintainer_replied = any(
            j["user"]["login"] in maintainers for j in journals
        )
        if not maintainer_replied:
            return {"truly_stale": True, "score": 0.9,
                    "reason": "用户提问后维护者从未回复"}
    
    # 评论内容关键词
    last_text = last_journal["notes"]
    if any(kw in last_text for kw in ["正在处理", "待 v2", "等待上游", "WIP"]):
        return {"truly_stale": False, "score": 0.8,
                "reason": "评论含'进行中'类关键词"}
    
    if any(kw in last_text for kw in ["已知问题", "占位", "暂不处理"]):
        return {"truly_stale": True, "score": 0.75,
                "reason": "评论含'已知/占位'类关键词"}
    
    return {"truly_stale": True, "score": 0.65, "reason": "默认判定为僵尸"}
```

### 2.2 Issue 类型信号

| tracker | 默认判断 | 例外 |
|---------|---------|------|
| bug | 谨慎处理（可能仍有效） | 若含"已修复，待 release"则跳过 |
| feature | 看评论活跃度 | 若是热门需求（≥ 5 👍）则跳过 |
| question | 大胆清理（多半已自然结束） | 若维护者问"还遇到吗？"而用户没回，必清理 |
| duplicate | 直接关闭 | - |
| support | 大胆清理 | - |
| doc | 看是否是 README 修正 | - |

**特殊情况**：

| tracker/标签 | 判断 |
|-------------|------|
| `roadmap` | **永不处理**（白名单） |
| `epic` | **永不处理**（白名单） |
| `security` | **永不处理**（白名单） |
| `pinned` | **永不处理**（白名单） |
| `in-progress` | **永不处理**（白名单） |
| `under-review` | **永不处理**（白名单） |

### 2.3 标签信号

```python
def check_labels(labels, action):
    """
    返回 (exempt, reason) 或 (False, None)
    """
    STALE_EXEMPT = {
        "pinned", "置顶",
        "security", "安全",
        "roadmap", "路线图",
        "epic", "里程碑",
        "in-progress", "进行中",
        "under-review", "审查中",
        "keep-open", "保留",
        "help-wanted",  # 等社区认领
        "good-first-issue",  # 等新手认领
    }
    
    for label in labels:
        if label.lower() in STALE_EXEMPT:
            return True, f"含豁免标签: {label}"
    
    return False, None
```

### 2.4 作者活跃度信号

```python
def analyze_author(author_login, repo_activity):
    """
    评估 Issue 作者的活跃度
    """
    author_issues = repo_activity["by_author"].get(author_login, [])
    
    if len(author_issues) == 1:
        # 路过用户：只此一个 Issue，可能是占位
        return {"stale_tendency": 0.7, "reason": "作者仅此 1 个 Issue"}
    
    if author_login in repo_activity["contributors"]:
        # 资深贡献者，信任会跟进
        return {"stale_tendency": 0.3, "reason": "作者是仓库贡献者"}
    
    if len(author_issues) >= 5:
        # 多 issue 用户，可能批量提交后不再跟进
        return {"stale_tendency": 0.6, "reason": f"作者历史 {len(author_issues)} 个 Issue"}
    
    return {"stale_tendency": 0.5, "reason": "中性"}
```

### 2.5 标题关键词信号

```yaml
keep_open_patterns:
  - "[WIP]"
  - "[Pinned]"
  - "[Keep Open]"
  - "路线图"
  - "长期"
  - "讨论"
  - "RFC"
  - "提案"

force_close_patterns:
  - "[已过期]"
  - "[占位]"
  - "测试"        # 仅 2 字符的"测试"
  - "测试用"
  - "ignore"
  - "deprecated"
```

---

## 3. 综合决策算法

### 3.1 信号汇总

```python
def ai_judge_stale(issue, journals, repo_meta):
    # 1. 时间过滤（前置）
    days = compute_days_inactive(issue)
    if days < stale_threshold:
        return {"truly_stale": False, "exempt": True,
                "reason": f"仅 {days} 天未活动，未达阈值"}
    
    # 2. 白名单豁免
    exempt, exempt_reason = check_labels(get_labels(issue), ...)
    if exempt:
        return {"truly_stale": False, "exempt": True, "reason": exempt_reason}
    
    # 3. 标题强信号
    if matches_force_close(issue.subject):
        return {"truly_stale": True, "confidence": 0.95,
                "reason": "标题含强制关闭关键词"}
    if matches_keep_open(issue.subject):
        return {"truly_stale": False, "confidence": 0.9,
                "reason": "标题含保留关键词"}
    
    # 4. 综合多信号
    signals = []
    
    # 4a. 评论历史信号（权重 0.4）
    j_signal = analyze_journals(journals, repo_meta.maintainers)
    signals.append(("journals", j_signal["score"], j_signal["reason"], 0.4))
    
    # 4b. 类型信号（权重 0.25）
    t_signal = analyze_tracker(issue.tracker_id)
    signals.append(("tracker", t_signal["score"], t_signal["reason"], 0.25))
    
    # 4c. 作者活跃度（权重 0.15）
    a_signal = analyze_author(issue.author, repo_meta)
    signals.append(("author", a_signal["stale_tendency"], a_signal["reason"], 0.15))
    
    # 4d. 时间长度（权重 0.2）
    time_score = min(1.0, (days - stale_threshold) / stale_threshold)
    signals.append(("time", time_score, f"{days} 天未活动", 0.2))
    
    # 5. 加权平均
    final_score = sum(score * weight for _, score, _, weight in signals)
    final_reason = "; ".join(f"{name}: {reason}" for name, _, reason, _ in signals)
    
    return {
        "truly_stale": final_score >= 0.6,
        "confidence": final_score,
        "reason": final_reason,
        "exempt": False
    }
```

### 3.2 置信度阈值

| confidence | 含义 | 建议动作 |
|-----------|------|---------|
| ≥ 0.85 | 极有把握 | 直接列入"建议执行"清单 |
| 0.7 - 0.85 | 较有把握 | 列入"建议执行"，报告中标记 |
| 0.6 - 0.7 | 一般 | 列入"建议复核" |
| < 0.6 | 把握不足 | **不自动处理**，仅列入"待人工"队列 |

---

## 4. 边界情况

| 情况 | 处理 |
|------|------|
| journals 数组很大 | 仅取最后 5 条用于 AI 判断 |
| 评论内容是图片/表情 | 跳过，仅看时间 |
| 评论是用户自己反复回（"up"、"催"） | 维护者从未回应 → 真僵尸 |
| 维护者评论是 "duplicate of #N" | 视为 duplicate，自动关闭 |
| 跨语言评论（中英混合） | 都能识别 |
| 评论含代码块 | 去除代码块后再分析 |

---

## 5. 示例分析

### 5.1 示例 A：真僵尸（高置信度）

```json
{
  "number": 142,
  "subject": "Bug: 登录页偶尔卡顿",
  "description": "有时候会卡...",
  "journals": [],
  "issue_tags": [],
  "author": {"login": "user-123"},
  "days_inactive": 68
}
```

**分析**：
- journals: 空 → 0.9
- tracker: bug → 0.5（中性）
- author: 仅此 1 个 Issue → 0.7
- time: 68 天 → 0.13

**加权**：`0.9*0.4 + 0.5*0.25 + 0.7*0.15 + 0.13*0.2 = 0.556`

**输出**：
```json
{
  "truly_stale": false,  // 略低于阈值
  "confidence": 0.556,
  "reason": "journals: 0 评论；tracker: bug 谨慎；author: 仅 1 Issue；time: 68 天",
  "recommended_action": "needs_review"
}
```

### 5.2 示例 B：误判避免（活跃）

```json
{
  "number": 156,
  "subject": "[Roadmap] v2 API 设计",
  "description": "长期讨论 v2 接口规范...",
  "journals": [
    {"user": "dev-li", "notes": "正在按这个方向重构", "created_at": "2026-06-15"},
    {"user": "dev-wang", "notes": "+1", "created_at": "2026-06-18"}
  ],
  "issue_tags": ["roadmap"],
  "days_inactive": 65
}
```

**分析**：
- 白名单：含 `roadmap` → **exempt: true**

**输出**：
```json
{
  "truly_stale": false,
  "exempt": true,
  "exempt_reason": "含豁免标签: roadmap",
  "recommended_action": "skip"
}
```

### 5.3 示例 C：明显僵尸（高置信度）

```json
{
  "number": 178,
  "subject": "测试",
  "description": "测试",
  "journals": [
    {"user": "user-1", "notes": "测试", "created_at": "2026-02-01"}
  ],
  "author": {"login": "user-1"},
  "days_inactive": 142
}
```

**分析**：
- 标题：含"测试"（force_close 模式）→ confidence 0.95
- author = last journal user → 用户自言自语
- time: 142 天

**输出**：
```json
{
  "truly_stale": true,
  "confidence": 0.95,
  "reason": "标题含强制关闭关键词",
  "recommended_action": "auto_close"
}
```

---

## 6. 信号权重调优

权重默认值（可在 Skill 配置中自定义）：

```yaml
signal_weights:
  journals: 0.4       # 评论历史最重要
  tracker: 0.25       # Issue 类型
  time: 0.2           # 时间长度
  author: 0.15        # 作者活跃度

confidence_thresholds:
  strong: 0.85        # 直接执行
  medium: 0.7         # 执行但标记
  weak: 0.6           # 待人工
```

**调优建议**：

- 团队项目：维护者评论信号最重要（提高 journals 权重）
- 开源项目：作者活跃度更关键（提高 author 权重）
- 紧急项目：时间长度更严格（提高 time 权重，降低阈值）

---

## 7. 与规则引擎的对比

| 维度 | 规则引擎（如 GitHub stale-bot） | AI 判断（本 Skill） |
|------|------------------------------|-------------------|
| 准确率 | ~70%（按时间一刀切） | ~90%（多信号综合） |
| 误关率 | 5-10% | < 2% |
| 配置复杂度 | YAML 写规则 | AI 自动理解上下文 |
| 可解释性 | 高（规则明确） | 中（reasoning 字段说明） |
| 性能 | 极快（无 AI 推理） | 中（需要 LLM 调用） |

**结论**：本 Skill 适合"宁可慢一点，也要少误关"的高质量项目。对于"堆积严重、宁可错杀"的清理任务，可在 SKILL.md 中临时调整 `stale_days` 和置信度阈值。
