# gitlink-gatekeeper REFERENCE

> 查字段 / 查算法的参考手册。工作流与裁决步骤见 [`SKILL.md`](./SKILL.md)，权威定义见 [`./REFERENCE.md`](./REFERENCE.md)（SSOT）。
> 本文与 SSOT 不一致时，**以 SSOT 为准**。所有数值、字段名、公式均逐条对齐 SSOT 第 2–7 节。

**CRITICAL — 认证、全局参数、GitLink 真实 API 坑见 [`gitlink-shared/SKILL.md`](../gitlink-shared/SKILL.md)。**
**CRITICAL — GitLink 平台只用 `gitlink-cli`，禁止 `gh`。**

---

## 1. `gatekeeper.yaml` 全字段参考

策略默认从仓库根目录 `gatekeeper.yaml` 读取，可用 `--policy <path>` 指定；找不到时回退到内置默认策略（即下表「默认值」列）。

### 1.1 顶层

| 字段 | 类型 | 默认值 | 含义 | 约束 |
|------|------|--------|------|------|
| `version` | int | `1` | 策略 schema 版本 | 当前固定为 `1` |
| `weights` | map | 见 §1.2 | 五维加权评分权重 | 五项之和 **必须 = 100** |
| `hard_gates` | map | 见 §1.3 | 硬门禁开关 | — |
| `severity_penalty` | map | 见 §1.4 | 严重度 → 扣分 | 仅用于 `review_findings` 维度 |
| `thresholds` | map | 见 §1.5 | 总分 → 裁决阈值 | `pass ≥ request_changes`，均 ∈ 0..100 |
| `labels` | map | 见 §1.6 | 三态裁决回写的标签名 | — |
| `source_globs` | list[str] | 见 §1.7 | 源码文件判定 glob | 用于 `test_coverage` / 测试硬门禁 |
| `test_globs` | list[str] | 见 §1.7 | 测试文件判定 glob | 同上 |
| `behavior` | map | 见 §1.8 | 行为开关 | — |

### 1.2 `weights`（五项之和必须 = 100）

| 字段 | 类型 | 默认值 | 含义 |
|------|------|--------|------|
| `review_findings` | int | `40` | AI 代码审查发现（按严重度扣分） |
| `test_coverage` | int | `20` | 改动的源码是否伴随测试 |
| `pr_hygiene` | int | `15` | PR 描述 / 关联 Issue / 体量 |
| `commit_quality` | int | `15` | commit 是否符合 Conventional Commits |
| `ci_status` | int | `10` | CI 是否通过 |

> 校验：`Σweights == 100`。不等于 100 应报错并拒绝运行（避免总分不再是 0–100）。

### 1.3 `hard_gates`（任一命中 → 直接 `REQUEST_CHANGES`，无视总分）

| 字段 | 类型 | 默认值 | 含义 |
|------|------|--------|------|
| `forbid_blocker_findings` | bool | `true` | 出现 blocker 级发现即拦截 |
| `require_ci_pass` | bool | `true` | CI 未通过即拦截 |
| `require_tests_for_src_changes` | bool | `true` | 改了源码却没加测试即拦截 |
| `require_linked_issue` | bool | `false` | PR 是否必须关联 Issue |
| `max_changed_files` | int | `80` | 超过该改动文件数即拦截；**`0` 表示不限** |

### 1.4 `severity_penalty`（用于 `review_findings`）

| 字段 | 类型 | 默认值 | 含义 |
|------|------|--------|------|
| `blocker` | int | `100` | 单条 blocker 即可把本维度清零（≥ 权重 40） |
| `major` | int | `25` | |
| `minor` | int | `5` | |
| `nit` | int | `1` | |

> `finding.severity` 取值域：`{blocker, major, minor, nit}`。未知严重度不应计入（视为 0 或报错），由 SKILL 侧约束 AI 只产出这四类。

### 1.5 `thresholds`

| 字段 | 类型 | 默认值 | 含义 | 约束 |
|------|------|--------|------|------|
| `pass` | int | `85` | 总分 ≥ `pass` 且无硬门禁失败 → `PASS` | 0..100 |
| `request_changes` | int | `60` | 总分 < `request_changes` → `REQUEST_CHANGES` | 0..100，且 `≤ pass` |

> 介于 `[request_changes, pass)` 之间 → `COMMENT`。

### 1.6 `labels`（依赖 `gitlink-cli label`）

| 字段 | 类型 | 默认值 | 对应裁决 |
|------|------|--------|----------|
| `pass` | str | `"gatekeeper:pass"` | `PASS` |
| `request_changes` | str | `"gatekeeper:needs-changes"` | `REQUEST_CHANGES` |
| `comment` | str | `"gatekeeper:review"` | `COMMENT` |

### 1.7 `source_globs` / `test_globs`

| 字段 | 类型 | 默认值 |
|------|------|--------|
| `source_globs` | list[str] | `["**/*.go", "**/*.py", "**/*.js", "**/*.ts", "**/*.rs", "**/*.java"]` |
| `test_globs` | list[str] | `["**/*_test.go", "**/test_*.py", "**/*.test.*", "**/*.spec.*", "tests/**"]` |

> 一个文件先按 `test_globs` 命中算测试；否则按 `source_globs` 命中算源码。`test_globs` 优先，避免 `foo_test.go` 同时被 `**/*.go` 计为源码。

### 1.8 `behavior`

| 字段 | 类型 | 默认值 | 含义 |
|------|------|--------|------|
| `dry_run_default` | bool | `true` | 默认只预览，不写任何东西（不传 `--apply` 即生效） |
| `post_comment` | bool | `true` | 把评分卡作为评论回写 PR |
| `apply_label` | bool | `true` | 按裁决打标签 |
| `auto_merge` | bool | `false` | **仅当 `true` 且裁决=PASS 且显式 `--apply` 时才合并** |
| `merge_method` | enum | `squash` | `merge` \| `rebase` \| `squash` |

---

## 2. 评分算法规格（确定性，可复现）

输入：一个 PR 的上下文（变更文件、diff、commits、CI 状态、PR 元信息）+ AI 审查产出的**发现列表**（每条带 `severity ∈ {blocker, major, minor, nit}`、`file`、`line`、`message`）。

每个维度产出 `0..weights[dim]` 的得分，五维相加得 `total ∈ 0..100`。同策略 + 同输入 → 同 total → 同裁决。

记号：`W.x` = `weights.x`，`SP[s]` = `severity_penalty[s]`。

### 2.1 `review_findings`（默认权重 40）

```
penalty = Σ over all findings  SP[finding.severity]
score   = W.review_findings * max(0, 1 - penalty / W.review_findings)
```

- 累计扣分达到该维度权重即扣到 0（不为负）。
- **边界（无发现）**：`penalty = 0 → score = W.review_findings`（满分）。
- **边界（含 blocker）**：单条 blocker 默认 `SP=100 ≥ 40`，本维度即清零；通常同时触发 `forbid_blocker_findings` 硬门禁。

### 2.2 `test_coverage`（默认权重 20）

```
changed_src   = 变更文件中匹配 source_globs 的数量
changed_tests = 变更文件中匹配 test_globs   的数量

if   changed_src == 0:     score = W.test_coverage          # 无源码改动，不扣，满分
elif changed_tests == 0:   score = 0                        # 改了源码但零测试
else:                      ratio = min(1, changed_tests / changed_src)
                           score = round(W.test_coverage * (0.5 + 0.5*ratio))
```

- **边界（无源码改动）**：纯文档 / 配置 PR → 满分（不应因没测试被罚）。
- 有测试即至少拿一半分；测试文件数 ≥ 源码文件数 → `ratio=1` → 满分。
- `round` 为四舍五入到整数。

### 2.3 `pr_hygiene`（默认权重 15，三项各占 1/3）

逐项累加命中比例 `hit ∈ {0, 1/6, 1/3, ...}`：

| 子项 | 命中条件 | 贡献 |
|------|----------|------|
| 描述 | PR 描述非空且长度 ≥ 30 字符 | +1/3 |
| 关联 Issue | PR body 含 `#<n>` 或 API 标记关联了 Issue | +1/3 |
| 体量 | `changed_files ≤ max_changed_files / 2` → +1/3；`max_changed_files/2 < changed_files ≤ max_changed_files` → +1/6；超上限 → +0 | +1/3 或 +1/6 或 0 |

```
score = round(W.pr_hygiene * hit_ratio)        # hit_ratio = 三项贡献之和
```

- **边界（`max_changed_files == 0` 表示不限）**：体量子项视为满足，记 +1/3。
- 体量子项与 `max_changed_files` 硬门禁独立计算；硬门禁只看是否 `> max_changed_files`。

### 2.4 `commit_quality`（默认权重 15）

```
conforming = 符合 Conventional Commits（type(scope): subject）的 commit 数
total      = commit 总数

if total == 0:  score = W.commit_quality                    # 满分
else:           score = round(W.commit_quality * conforming / total)
```

- **边界（`total == 0`）**：取不到 commit（API 空 / 异常）时给满分，不因数据缺失惩罚。
- Conventional Commits 判定：`type(optional-scope): subject`，常见 `type` ∈ {feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert}（`scope` 可省略）。

### 2.5 `ci_status`（默认权重 10）

```
CI 通过      → score = W.ci_status
CI 失败      → score = 0
CI 未知 / 无 → score = round(W.ci_status * 0.5)
```

- **边界（CI 未知/无）**：拿一半分，避免对没有 CI 的仓库一刀切清零。
- 注意与硬门禁的区别：`require_ci_pass` 只在 CI **明确失败**时拦截；CI 未知不触发硬门禁，但本维度仅得半分。

---

## 3. 硬门禁判定表（SSOT §4）

按 `hard_gates` 逐项判定，命中任一 → `hard_gate_failed = true`：

| 门禁字段 | 触发条件 | 失败说明（写入评分卡） |
|----------|----------|------------------------|
| `forbid_blocker_findings` | `true` 且存在 ≥1 条 blocker 发现 | 存在 blocker 级问题 |
| `require_ci_pass` | `true` 且 CI **未通过**（失败；未知不触发） | CI 未通过 |
| `require_tests_for_src_changes` | `true` 且 `changed_src > 0` 且 `changed_tests == 0` | 改了源码但无测试 |
| `require_linked_issue` | `true` 且 PR 未关联 Issue | 未关联 Issue |
| `max_changed_files` | `> 0` 且 `changed_files > max_changed_files` | 改动文件数超上限 |

> `max_changed_files == 0` → 该门禁直接跳过（不限）。多项命中时全部记入评分卡的 Hard gate failures 区块。

---

## 4. 裁决伪代码（SSOT §5）

```python
def decide(total, hard_gate_failed, thresholds):
    if hard_gate_failed:
        return "REQUEST_CHANGES"          # 硬门禁优先，无视总分
    if total >= thresholds.pass:
        return "PASS"
    if total < thresholds.request_changes:
        return "REQUEST_CHANGES"
    return "COMMENT"                        # total ∈ [request_changes, pass)
```

裁决映射（用于评分卡标题与回写）：

| verdict | emoji | 回写标签（默认） | 是否允许合并 |
|---------|:-----:|------------------|--------------|
| `PASS` | ✅ | `gatekeeper:pass` | 仅当 `behavior.auto_merge==true` 且显式 `--apply` |
| `REQUEST_CHANGES` | ❌ | `gatekeeper:needs-changes` | 否 |
| `COMMENT` | 💬 | `gatekeeper:review` | 否 |

> 评分卡 Markdown 模板见 SSOT §6 / SKILL.md，本文不重复。

---

## 5. CLI 命令映射速查（SSOT §7，已对 `shortcuts/` 逐条核验）

| 步骤 | 数据 | 命令 | 备注 |
|------|------|------|------|
| PR 元信息 | 标题/描述/作者/关联 | `gitlink-cli pr +view -i <id> --format json` | `+view`，`-i`/`--id` |
| 变更文件 | 文件路径列表 | `gitlink-cli pr +files -i <id> --format json` | `+files` |
| Diff | 变更内容供 AI 审查 | `gitlink-cli pr +diff -i <id> --format json` | `+diff`；当前实现与 `+files` 命中同一 `/pulls/:id/files` 端点 |
| commits | commit 列表 | `gitlink-cli api GET /:owner/:repo/pulls/:id/commits --format json` | Raw API（无对应 shortcut） |
| CI 状态 | 构建结果 | `gitlink-cli ci +builds --format json` | `+builds`（`-p`/`-l` 分页） |
| 回写评论 | 评分卡 | `gitlink-cli pr +comment -i <id> -b "<scorecard>"` | `+comment` 底层走 issue journals（评论流）；评审记录形式用 `pr +review -i <id> -s common -c "..."`（走 reviews 端点，payload 字段是 `content`/`status`，status 取 `common`/`approved`/`rejected`）。评分卡作为建议性回写，二者均用 `common` |
| 创建标签 | 裁决标签 | `gitlink-cli label +create -n "<name>" -c "#RRGGBB"` | `+create`（本作品子题一新增；`label +list/+update/+delete` 同组） |
| 挂标签 | 把标签挂到 PR 对应 Issue | `ISSUE_ID=$(gitlink-cli pr +view -i <pr> --format json \| jq -r '.data.issue.id')` 后 `gitlink-cli api POST /:owner/:repo/issues/$ISSUE_ID --body '{"issue_tag_ids":[<id>],"done_ratio":0,"subject":"...","description":"..."}'` | `:id` 是 PR 关联的 **issue.id（非 PR 号）**；更新须带回 `subject`/`description`（见 gitlink-shared）。亦可用 `gitlink-cli issue +update`（自动保留 subject/description） |
| 合并（受限） | 仅 PASS+`auto_merge`+`--apply` | `gitlink-cli pr +merge -i <id> --method <merge_method>` | **CLI 标志为 `--method`/`-m`**（值 `merge`/`rebase`/`squash`）；底层 API payload 字段才叫 `do`（不要当 CLI flag 用） |

### 真实 API 注意（GitLink 平台特性，gatekeeper 直接受影响）

| 坑 | 对 gatekeeper 的影响 |
|----|----------------------|
| PR review `status` 为 `common`/`approved`/`rejected`（非 GitHub 的 event=COMMENT/APPROVE） | gatekeeper 刻意把自动裁决统一以建议性 `common` 回写（或用 `pr +comment`），强语义 `approved`/`rejected` 留给人工；三态裁决靠评分卡标题 + `gatekeeper:*` 标签表达状态 |
| PR list `--state` 过滤不准 | 列 PR 时用返回体 `pull_request_status` 字段客户端过滤（0=open, 1=merged, 2=closed） |
| Issue 更新需带 `subject`/`description`/`done_ratio` | 经 `issue_tag_ids` 挂标签时，Raw API 须先 GET 再回传这些字段，否则可能清空描述 |
| `create_file` 的 `content` 须 base64 | 子题三创建 tracking issue 若需落文件时遵守 |
| 分支删除 API 是平台 bug | gatekeeper 不依赖删分支；勿用 `DELETE .../branches/:name` |
| GitLink 主分支为 `master` | 合并 base、`pr +create --base` 默认 `master` |

> 写操作前必须向用户复述将要做什么；不传 `--apply` 时一律 dry-run（`behavior.dry_run_default`）。Token 不回显。
