Merge pull request 'feat(skills): add gitlink-gatekeeper — Policy-as-Code PR merge gate' (#90) from recorder/gitlink-cli:feat/gatekeeper-skill into master

This commit is contained in:
wbtiger 2026-06-08 02:22:00 +08:00
commit f3e5d4aba0
11 changed files with 1682 additions and 0 deletions

View File

@ -153,6 +153,7 @@ skills/
| **gitlink-pm** | 项目管理 | 通过 Raw API 访问 |
| **gitlink-workflow** | AI 工作流 | Issue 分类、PR Review、Release Notes |
| **gitlink-health** | 开源项目健康度 | 详情见SKILL.md |
| **gitlink-gatekeeper** | Policy-as-Code PR 合并门禁:按 `gatekeeper.yaml` 策略算 0-100 评分卡 + 三态裁决PASS/REQUEST_CHANGES/COMMENT默认 dry-run、绝不自动合并 | `pr +view/+files/+diff`, `pr +comment`, `label +create` |
---

View File

@ -0,0 +1,244 @@
# gitlink-gatekeeper REFERENCE
> 查字段 / 查算法的参考手册。工作流与裁决步骤见 [`SKILL.md`](./SKILL.md),权威定义见 [`./REFERENCE.md`](./REFERENCE.md)SSOT
> 本文与 SSOT 不一致时,**以 SSOT 为准**。所有数值、字段名、公式均逐条对齐 SSOT 第 27 节。
**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 应报错并拒绝运行(避免总分不再是 0100
### 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 Commitstype(scope): subject的 commit 数
total = commit 总数
if total == 0: score = W.commit_quality # 满分
else: score = round(W.commit_quality * conforming / total)
```
- **边界(`total == 0`**:取不到 commitAPI 空 / 异常)时给满分,不因数据缺失惩罚。
- 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 不回显。

View File

@ -0,0 +1,370 @@
---
name: gitlink-gatekeeper
version: 1.0.0
description: "Policy-as-Code 的 PR 合并门禁:按版本化的 gatekeeper.yaml 策略对一个 Pull Request 聚合多路信号、算出 0-100 评分卡、给出三态裁决PASS / REQUEST_CHANGES / COMMENT并把结论作为结构化评论回写、打标签。当用户需要判断 PR 能否合并、做合并门禁/质量闸门、生成可复现的 PR 裁决,或问『这个 PR 达标了吗』时触发。默认 dry-run绝不自动合并。"
metadata:
requires:
bins: ["gitlink-cli"]
cliHelp: "gitlink-cli pr --help"
---
# gitlink-gatekeeperPolicy-as-Code PR 合并门禁)
**CRITICAL — 开始前必须先阅读 [`gitlink-shared/SKILL.md`](../gitlink-shared/SKILL.md),其中包含认证、权限处理和 GitLink API 注意事项PR list state 过滤不准、主分支是 master、issue 更新需带回 subject/description 等)。**
**CRITICAL — 任何写操作(回写评论、打标签、合并)前,必须向用户复述将要做什么并得到确认;默认 dry-run 不写任何东西。绝不默认自动合并。**
**CRITICAL — GitLink 操作只能用 `gitlink-cli`。禁止用 `gh`GitHub CLI操作 GitLink 资源。`gh` 仅适用于 GitHub 平台。**
> **前置条件:** 先阅读 [`gitlink-shared/SKILL.md`](../gitlink-shared/SKILL.md) 了解认证和全局参数;本 Skill 的全字段策略与算法细节见同目录 [`REFERENCE.md`](./REFERENCE.md),故障排查见 [`TROUBLESHOOTING.md`](./TROUBLESHOOTING.md)。
---
## 定位与产物
gitlink-gatekeeper 是一个**可复现的 PR 合并门禁**:团队把合并标准写进版本化的 `gatekeeper.yaml`gatekeeper 据此对一个 PR 算出**透明的 0-100 评分卡**,给出**三态裁决**,并把结论回写。与 `gitlink-code-review`(只产出主观评论、无阈值、无结论)相比,本 Skill 的核心是**可裁决、可审计、可复现**:同一策略 + 同一 PR → 同一裁决。
| 裁决 | emoji | 触发条件 | 回写标签 |
|------|:----:|----------|----------|
| PASS | ✅ | 无硬门禁失败 且 `total ≥ thresholds.pass` | `gatekeeper:pass` |
| REQUEST_CHANGES | ❌ | 命中任一硬门禁,或 `total < thresholds.request_changes` | `gatekeeper:needs-changes` |
| COMMENT | 💬 | 介于两阈值之间且无硬门禁失败 | `gatekeeper:review` |
> GitLink 原生 PR review 的 `status``common`/`approved`/`rejected``rejected` 即「请求修改」的等价。gitlink-gatekeeper **刻意把所有自动裁决以建议性的 `common` 评论回写**(评分卡标题明确标注三态裁决),把强语义的 `approved`/`rejected` 留给人工授意;裁决状态通过标签(`gatekeeper:needs-changes` 等)承载——这正是本作品复用 `label` 命令组的原因。
---
## 工作流概览
| 阶段 | 操作 | AI Agent 角色 |
|------|------|--------------|
| ① 加载策略 | 读 `gatekeeper.yaml`,找不到则回退内置默认策略 | 解析 / 校验 / 回退 |
| ② 采集上下文 | 拉 PR 元信息、变更文件、diff、commits、CI 状态 | 执行 CLI 命令采集数据 |
| ③ 产出发现 | 逐文件审查,按 severity 分级标记问题 | AI 分析,输出发现列表 |
| ④ 逐维评分 | 五维各算 `0..weight` 得分,相加得 `total` | 确定性计算(非主观) |
| ⑤ 硬门禁 | 逐项判定 `hard_gates`,命中即拦截 | 布尔判定 |
| ⑥ 裁决 | 由硬门禁 + total + 阈值得出三态 | 确定性计算 |
| ⑦ 渲染评分卡 | 按模板渲染 Markdown 评分卡 | 模板填充 |
| ⑧ 回写(受 `--apply` | dry-run 仅打印;`--apply` 才回写评论 + 打标签 | 写操作,需确认 |
| ⑨ 安全规则 | 默认 dry-run绝不自动合并 | 守门 |
---
## 详细工作流
### Step ① 加载策略 gatekeeper.yaml
策略默认从**仓库根目录** `gatekeeper.yaml` 读取,可用 `--policy <path>` 指定。找不到时**回退到下文内置默认策略**(务必在评分卡 footer 注明用的是哪个策略来源)。
加载后做两项校验(不通过则报错并停止):
- `weights` 五项之和必须 **= 100**`review_findings + test_coverage + pr_hygiene + commit_quality + ci_status`)。
- `thresholds.pass`、`thresholds.request_changes` 为 0-100 整数,且 `pass ≥ request_changes`
**内置默认策略**(与 `examples/gatekeeper.yaml` 一致回退时使用policy 标记为 `<built-in default>@v1`
```yaml
version: 1
weights:
review_findings: 40
test_coverage: 20
pr_hygiene: 15
commit_quality: 15
ci_status: 10
hard_gates:
forbid_blocker_findings: true
require_ci_pass: true
require_tests_for_src_changes: true
require_linked_issue: false
max_changed_files: 80
severity_penalty: { blocker: 100, major: 25, minor: 5, nit: 1 }
thresholds: { pass: 85, request_changes: 60 }
labels:
pass: "gatekeeper:pass"
request_changes: "gatekeeper:needs-changes"
comment: "gatekeeper:review"
source_globs: ["**/*.go", "**/*.py", "**/*.js", "**/*.ts", "**/*.rs", "**/*.java"]
test_globs: ["**/*_test.go", "**/test_*.py", "**/*.test.*", "**/*.spec.*", "tests/**"]
behavior:
dry_run_default: true
post_comment: true
apply_label: true
auto_merge: false
merge_method: squash
```
### Step ② 采集 PR 上下文CLI 命令映射)
用以下命令采集一个 PR 的全部输入。**始终带 `--format json` 便于解析**`--owner`/`--repo` 在 git 仓库目录下可自动从 remote 解析,否则显式传入。
| 步骤 | 数据 | 命令 |
|------|------|------|
| PR 元信息 | 标题/描述/作者/关联 issue | `gitlink-cli pr +view -i <id> --format json` |
| 变更文件 | 文件路径列表 | `gitlink-cli pr +files -i <id> --format json` |
| Diff | 变更内容供 AI 审查 | `gitlink-cli pr +diff -i <id> --format json` |
| commits | commit 列表(消息供 commit_quality | `gitlink-cli api GET /:owner/:repo/pulls/:id/commits --format json` |
| CI 状态 | 构建结果 | `gitlink-cli ci +builds --format json` |
> 实测注意:`pr +files` 与 `pr +diff` 底层都打 `/pulls/:id/files`——`+files` 取路径列表,`+diff` 取含 patch 的同一份数据,按需取用即可。无 `pr +commits` 快捷命令commit 列表只能走 Raw API。CI 通过/失败需从 builds 返回的 `status` 字段判断;**无 build 记录时按「CI 未知」处理**(见 §3.5)。
```bash
PR=42
gitlink-cli pr +view -i "$PR" --format json # title / body / 关联 issue
gitlink-cli pr +files -i "$PR" --format json # changed files
gitlink-cli pr +diff -i "$PR" --format json # diff供 AI 审查)
gitlink-cli api GET /:owner/:repo/pulls/$PR/commits --format json
gitlink-cli ci +builds --format json
```
### Step ③ AI 按 severity 分级产出发现
对 diff 逐文件审查(检查项参考 `gitlink-code-review`:安全红线、错误处理、并发、资源管理、魔法数字、测试覆盖等),产出**结构化发现列表**,每条带:
```
{ severity: blocker|major|minor|nit, file: "<path>", line: <n>, message: "<问题描述>" }
```
severity 判定准则(与 §3.1 扣分表对应):
| severity | 含义 | 典型例子 |
|----------|------|----------|
| `blocker` | 必须拦截,单条即清零本维度并触发硬门禁 | 硬编码密钥/Token、SQL/命令注入、路径遍历、不安全反序列化、XSS |
| `major` | 严重缺陷,强烈建议修 | 未处理错误/异常吞掉、资源泄漏、并发竞态、明显逻辑错误 |
| `minor` | 一般问题 | 风格偏离、轻微复杂度、缺注释、魔法数字 |
| `nit` | 吹毛求疵 | 命名小瑕疵、格式建议 |
> 控制信噪比:宁可少而准。同时记录 `Strengths`(做得好的点),用于评分卡 `✅ Strengths` 区。
### Step ④ 按 SSOT 算法逐维评分(确定性)
每维产出 `0..weights[dim]`,五维相加得 `total ∈ 0..100`。**这是确定性计算,不是再次主观打分**——同输入必同分。
**4.1 review_findings默认 40**
```
penalty = Σ severity_penalty[finding.severity] # 对所有发现求和
score = weights.review_findings * max(0, 1 - penalty / weights.review_findings)
```
扣分累计达本维度权重即扣到 0blocker 单条penalty=100即清零本维度。
**4.2 test_coverage默认 20**
```
changed_src = 变更文件中匹配 source_globs 的数量
changed_tests = 变更文件中匹配 test_globs 的数量
if changed_src == 0: score = weights.test_coverage # 无源码改动,不扣
elif changed_tests == 0: score = 0
else: ratio = min(1, changed_tests / changed_src)
score = round(weights.test_coverage * (0.5 + 0.5*ratio)) # 有测试至少拿一半
```
**4.3 pr_hygiene默认 15三项各占 1/3**
- 描述非空且长度 ≥ 30 字符 → +1/3
- 关联了 IssuePR body 含 `#<n>` 或 API 标记)→ +1/3
- 体量适中(`changed_files ≤ max_changed_files/2`)→ +1/3超过一半但未超上限 → +1/6
```
score = round(weights.pr_hygiene * 命中比例)
```
**4.4 commit_quality默认 15**
```
conforming = 符合 Conventional Commits (type(scope): subject) 的 commit 数
total_c = commit 总数
score = round(weights.commit_quality * conforming / total_c) # total_c=0 给满分
```
**4.5 ci_status默认 10**
```
CI 通过 → weights.ci_status
CI 失败 → 0
CI 未知/无 → round(weights.ci_status * 0.5)
```
### Step ⑤ 硬门禁评估
`hard_gates` 逐项判定,命中任一 → `hard_gate_failed = true`(记下命中的门禁名供评分卡 `⛔ Hard gate failures` 区):
| 门禁 | 命中条件 |
|------|----------|
| `forbid_blocker_findings` | 为 true 且存在 blocker 发现 |
| `require_ci_pass` | 为 true 且 CI **明确失败**`failing``unknown`/无 build 记录**不触发**本门禁 |
| `require_tests_for_src_changes` | 为 true 且 `changed_src > 0``changed_tests == 0` |
| `require_linked_issue` | 为 true 且未关联 Issue |
| `max_changed_files` | `> 0``changed_files > max_changed_files` |
### Step ⑥ 裁决计算
```
if hard_gate_failed: verdict = REQUEST_CHANGES
elif total >= thresholds.pass: verdict = PASS
elif total < thresholds.request_changes: verdict = REQUEST_CHANGES
else: verdict = COMMENT
```
### Step ⑦ 渲染评分卡(回写到 PR 的 Markdown
按下列模板填充emojiPASS=✅REQUEST_CHANGES=❌COMMENT=💬)。无硬门禁失败时省略 `⛔` 区;各发现区无内容时省略。
```markdown
## 🛡️ Gatekeeper Report — PR #<id> <title>
**Verdict: <emoji> <PASS|REQUEST_CHANGES|COMMENT>** · Score: <total>/100 · policy: <policy_path>@v<version>
| Dimension | Weight | Score | Notes |
|-----------|:------:|:-----:|-------|
| Review findings | 40 | <s>/40 | <n blocker / n major / n minor / n nit> |
| Test coverage | 20 | <s>/20 | <changed_src> src / <changed_tests> test files |
| PR hygiene | 15 | <s>/15 | <desc / linked issue / size 命中情况> |
| Commit quality | 15 | <s>/15 | <conforming>/<total> conventional |
| CI status | 10 | <s>/10 | <passing/failing/unknown> |
### ⛔ Hard gate failures (<n>)
- `<gate>`: <说明>
### 🔴 Must fix (<n>)
- [<severity>] <message><file>:<line>
### 🟡 Should fix (<n>)
- ...
### 🔵 Nits (<n>)
### ✅ Strengths
- <做得好的点>
### Next steps
1. <按裁决给出的最高优先级行动>
---
*Generated by gitlink-gatekeeper · policy-as-code PR gate · re-run after changes*
```
### Step ⑧ 回写dry-run默认vs --apply
**默认 dry-run**:不传 `--apply` 时,只把评分卡**打印给用户**,不回写、不打标签、不合并。
**`--apply` 才写**:写操作前**先向用户复述**「将向 PR #<id> 回写评分卡评论 + 打标签 `<label>`」,确认后执行。
回写评论(二选一,优先用 `pr +review` 因其自带 `--dry-run` 预览):
```bash
# 方式 A作为 PR review 回写status 用 common即 GitLink 的 COMMENT 等价)
gitlink-cli pr +review -i "$PR" --status common --content "$(cat scorecard.md)"
# 方式 B作为普通 PR 评论回写
gitlink-cli pr +comment -i "$PR" --body "$(cat scorecard.md)"
```
> ⚠️ `pr +review --status` 只接受 `common` / `approved` / `rejected`。评分卡评论一律用 `common`(不要因为裁决=PASS 就 `approved`APPROVE 是更强的批准语义,需用户显式授意)。裁决信息已写在评分卡标题里,状态由标签承载。
打标签GitLink 无「直接给 PR 挂标签」的命令,标签挂在 PR 背后的 Issue 上):
```bash
# 1) 确保裁决对应的标签存在(首次需创建;已存在则跳过)
gitlink-cli label +list --format json # 查现有标签拿 id
gitlink-cli label +create --name "gatekeeper:needs-changes" --color "#D73A4A" \
--description "PR gate: changes requested" # 不存在才创建
# 2) 取 PR 背后的 issue idpr +view 返回里有 issue 对象)
ISSUE_ID=$(gitlink-cli pr +view -i "$PR" --format json | jq -r '.data.issue.id')
# 3) 通过 Issue 更新挂标签(必须带回当前 subject/description否则会被清空——见 gitlink-shared
gitlink-cli api POST /:owner/:repo/issues/$ISSUE_ID --body '{
"issue_tag_ids": [<tag_id>],
"done_ratio": 0,
"subject": "<原始标题>",
"description": "<原始描述>"
}'
```
### Step ⑨ 安全规则(硬性,不可绕过)
- **默认 dry-run**:不传 `--apply` 一律只打印,不产生任何写副作用。
- **绝不默认自动合并**`behavior.auto_merge` 默认 `false`;即便策略里设为 `true`,也必须**同时**满足 `verdict == PASS` **且**命令显式带 `--apply` 才允许合并,且合并前再次向用户复述确认。
- 合并命令(仅在上述全部条件满足时):
```bash
# merge_method 来自策略 behavior.merge_methodmerge|rebase|squash
gitlink-cli pr +merge -i "$PR" --method squash
```
> 注意 CLI 标志是 `--method`(底层 API 字段才是 `do`)。
- 不回显 Token遵循 `gitlink-shared/SKILL.md` 的认证与 API 注意事项401 引导重登、403 查权限)。
- REQUEST_CHANGES 永不触发合并COMMENT/PASS 默认也不合并,除非满足自动合并三条件。
---
## 完整示例
### 示例 1dry-run默认安全不写任何东西
```bash
# 在目标仓库目录下,对 PR #42 跑门禁,仅预览评分卡
PR=42
gitlink-cli pr +view -i "$PR" --format json
gitlink-cli pr +files -i "$PR" --format json
gitlink-cli pr +diff -i "$PR" --format json
gitlink-cli api GET /:owner/:repo/pulls/$PR/commits --format json
gitlink-cli ci +builds --format json
# → AI 产出发现 → 按 §4 评分 → §5 硬门禁 → §6 裁决 → §7 渲染评分卡
# → dry-run仅把评分卡打印给用户结尾提示「如需回写到 PR请加 --apply」
```
预期产出(节选):
```
**Verdict: ❌ REQUEST_CHANGES** · Score: 58/100 · policy: gatekeeper.yaml@v1
⛔ Hard gate failures (1)
- require_tests_for_src_changes: 改了 3 个源码文件但没有新增测试
dry-run未回写、未打标签、未合并
```
### 示例 2--apply用户确认后回写评论 + 打标签)
```bash
PR=42
# …(同上采集 + 评分,得到裁决=REQUEST_CHANGES渲染出 scorecard.md)…
# 写操作前向用户复述:将向 PR #42 回写评分卡评论 + 打标签 gatekeeper:needs-changes
# 用户确认后:
# 1) 回写评分卡review 形式,自带 dry-run 可先预演)
gitlink-cli pr +review -i "$PR" --status common --dry-run --content "$(cat scorecard.md)" # 预演
gitlink-cli pr +review -i "$PR" --status common --content "$(cat scorecard.md)" # 实际回写
# 2) 打标签 gatekeeper:needs-changes按 Step ⑧ 取/建 tag_id 与 issue_id 后)
gitlink-cli api POST /:owner/:repo/issues/$ISSUE_ID --body '{
"issue_tag_ids": [101], "done_ratio": 0,
"subject": "feat: add rate limiter", "description": "<原始描述>"
}'
# 注意:裁决=REQUEST_CHANGES → 绝不合并。
# 即便裁决=PASS也只有在 policy.auto_merge=true 且本次显式 --apply 时才允许:
# gitlink-cli pr +merge -i "$PR" --method squash
```
---
## 策略预设
`examples/` 提供三套可直接 `--policy` 引用的预设:
| 预设 | 特点 | 适用 |
|------|------|------|
| `gatekeeper.yaml` | 默认平衡策略pass 85 / rc 60 | 大多数仓库 |
| `gatekeeper.strict.yaml` | 高阈值、`require_linked_issue: true`、更小 `max_changed_files` | 核心库 / 发布分支 |
| `gatekeeper.lenient.yaml` | 低阈值、关掉部分硬门禁 | 早期项目 / 文档仓库 |
```bash
gitlink-cli pr +view -i 42 --format json # 采集后用严格策略评分(评分逻辑同上,仅阈值/门禁不同)
# 评分时加载:--policy examples/gatekeeper.strict.yaml
```
---
## 最佳实践
1. **先 dry-run 再 `--apply`**:永远先看评分卡内容,确认无误再回写。
2. **策略进版本库**:把 `gatekeeper.yaml` 提交到仓库,让裁决标准对所有贡献者透明、可审计。
3. **可复现优先**:评分是确定性的——若两次裁决不同,先查是不是 PR 内容或策略变了而非「AI 心情」。
4. **控制发现数量**:最严重的 3-5 条比 20 条琐碎问题更有价值nit 折叠展示。
5. **大 PR 分段处理**`pr +diff` 输出可能很大Agent 应分段读取 diff 再汇总发现。
6. **标签复用**:同一仓库的三个 `gatekeeper:*` 标签建一次即可,后续只更新挂载关系。
## 注意事项
- PR review/评论提交后会通知关注该 PR 的参与者,评分卡内容保持专业、可操作。
- `--state` 过滤 PR 列表不精确,需用 `pull_request_status` 字段客户端判断0=open,1=merged,2=closed
- GitLink 主分支是 `master`(非 `main`);合并方式由策略 `merge_method` 决定。
- 对 draft PR 应提示用户先标记为 Ready for Review 再门禁。
- 端到端「PR 看门人闭环」(路由建议 reviewer → 裁决 → 回写 + 为 REQUEST_CHANGES 自动建 tracking issue的完整工作流形态见配套独立仓库 recorder/gitlink-gatekeeper策略字段与算法见 `REFERENCE.md`

View File

@ -0,0 +1,319 @@
# gitlink-gatekeeper — 故障排查TROUBLESHOOTING
**CRITICAL — 开始前请先阅读 [`gitlink-shared/SKILL.md`](../gitlink-shared/SKILL.md)(认证、全局参数、真实 API 坑)与 [`./SKILL.md`](./SKILL.md)(工作流)、[`./REFERENCE.md`](./REFERENCE.md)(策略字段与评分算法)。**
**CRITICAL — gatekeeper 默认 dry-run绝不自动合并。本文档中任何「修复」都不应放宽该安全默认除非用户明确要求。**
**CRITICAL — GitLink 资源只能用 `gitlink-cli` 操作,禁止用 `gh`/`glab`。**
本文档列出运行 gatekeeper 时的常见问题,按「症状 / 原因 / 解决」三段式给出。先看下表速查,再到对应小节读细节。
## 速查表
| # | 症状 | 根因 | 一句话解决 |
|---|------|------|-----------|
| 1 | 提示「未找到策略文件」 | 仓库根无 `gatekeeper.yaml` 且未传 `--policy` | 回退内置默认策略,或 `--policy <path>` 指定 |
| 2 | 报「weights 之和 ≠ 100」 | 五维权重总和不是 100 | 调整权重让 `review+test+hygiene+commit+ci = 100` |
| 3 | `401`(请登录)/ `403`(无权限) | Token 过期 / owner-repo 错或无权限 | `gitlink-cli auth login` / 核对 owner、repo、权限 |
| 4 | `pr +diff` 输出巨大、超出处理窗口 | 大型 PR 的 diff 一次性返回太大 | 改用 `pr +files` 选关键文件 + `version-diff -f` 按文件分段 |
| 5 | 评分卡里 CI 显示 `unknown` | `ci +builds` 取不到与该 PR 对应的构建 | 按 unknown 计 0.5×权重,不要当失败 |
| 6 | 如何表达 REQUEST_CHANGES 裁决 | 自动门禁刻意不替人按 `approved`/`rejected` | 用建议性 `common` 评论 + 标题标注裁决 + 打 `gatekeeper:needs-changes` 标签表达 |
| 7 | 打标签报「标签不存在」 | 该 label 尚未在仓库创建 | 先 `label +list` 查,缺则 `label +create` 再挂到 issue |
| 8 | `auto_merge` 开了却没合并 | 三条件未同时满足 | 需 `verdict==PASS` + 命令带 `--apply` + 策略 `auto_merge: true` |
| 9 | 评分卡对 draft PR 给出裁决 | 草稿 PR 不应被门禁裁决 | 检测 draft 标志,提示先转 Ready for Review |
| 10 | 大仓库只看到前 20 条 PR/构建/标签 | 列表接口默认分页 limit=20 | 用 `--page` / `--limit` 翻页,按 `meta.total_count` 判完整 |
| 11 | `--state open` 仍返回已合并/关闭的 PR | GitLink `--state` 仅影响计数 | 用返回里的 `pull_request_status` 客户端过滤 |
| 12 | `pr +merge``--do` 不是已知 flag | flag 名记错(`do` 是底层 API 字段) | CLI flag 是 `--method`/`-m`,不是 `--do` |
---
## 1. 找不到 `gatekeeper.yaml`(回退默认)
**症状**:运行时提示「未在仓库根目录找到 `gatekeeper.yaml`」,或评分卡页脚 `policy:` 显示为内置默认而非你期望的文件。
**原因**:策略文件默认从**仓库根目录**的 `gatekeeper.yaml` 读取。当根目录没有该文件、且命令未带 `--policy <path>`gatekeeper 不会报错中止,而是按设计**回退到内置默认策略**(见 `REFERENCE.md` 的默认值:`pass=85 / request_changes=60`,权重 `40/20/15/15/10`)。
**解决**
- 若你确实想用默认策略 —— 这是正常行为,无需处理;评分卡页脚会标 `policy: built-in@v1`
- 若你有自定义策略 —— 确认文件确实在仓库根,或显式指定路径:
```bash
# 显式指定策略文件(路径相对当前工作目录)
gitlink-gatekeeper --policy ./policies/gatekeeper.strict.yaml --pr 42
# 把示例策略复制到仓库根,让默认查找命中
cp skills/gitlink-gatekeeper/examples/gatekeeper.yaml ./gatekeeper.yaml
```
> 注意:策略文件路径区分大小写;`Gatekeeper.yaml`、`gatekeeper.yml``.yml` 后缀)都不会被默认查找命中。
---
## 2. `weights` 之和不等于 100
**症状**加载策略时报错「weights 之和必须为 100当前为 `<n>`gatekeeper 拒绝按该策略评分。
**原因**:评分算法要求五个维度权重之和**严格等于 100**,这样 `total ∈ 0..100` 才有可比性与可复现性。常见错误是只改了一两个维度、忘了让其余维度补平。
**解决**:调整 `weights`,使五项相加正好 100。
```yaml
weights:
review_findings: 40
test_coverage: 20
pr_hygiene: 15
commit_quality: 15
ci_status: 10 # 40+20+15+15+10 = 100 ✅
```
| 错误示例 | 和 | 问题 |
|----------|:--:|------|
| `40/20/15/15/5` | 95 | 少 5需补到某一维度 |
| `50/20/15/15/10` | 110 | 多 10把 review 降回 40 |
> 注意:维度名必须是这五个固定键(`review_findings`/`test_coverage`/`pr_hygiene`/`commit_quality`/`ci_status`),多写或漏写键也会校验失败。某维度想「不计分」应把权重设 0 并把差额加到别处,而不是删除该键。
---
## 3. `401` / `403` 认证与权限错误
**症状**:任意数据采集命令返回
```json
{"ok": false, "error": {"code": 401, "message": "请登录后再操作", "suggestion": "请先运行 gitlink-cli auth login 登录"}}
```
`403`(拒绝访问)。
**原因**(详见 `gitlink-shared/SKILL.md`「认证错误处理」):
- `401`:未登录或 Token 已过期。GitLink Token 有效期 **7 天**,过期需重新登录。
- `403`:已登录但对该 `owner/repo` 无权限,或 owner/repo 解析错误。
**解决**
```bash
# 401重新登录
gitlink-cli auth login
gitlink-cli auth status # 确认登录态
# 403核对上下文在仓库目录下可从 git remote 自动解析)
gitlink-cli repo +info --owner <owner> --repo <repo> --format json
```
> 注意gatekeeper 全程**不回显 Token**。若 `auth status` 正常但仍 `403`,多半是 owner/repo 写错或你只有读权限——读权限足够采集与生成评分卡dry-run但写评论、打标签、合并需要写权限。
---
## 4. PR diff 过大需分段
**症状**`gitlink-cli pr +diff -i <id> --format json` 返回内容极大,超出单次处理窗口,或采集很慢/报频率限制。
**原因**:大型 PR 的全量 diff 一次返回会非常大(`gitlink-shared` 与 `gitlink-code-review` 均提示 `pr +diff` 输出可能很大需分段处理files/diff 接口有频率限制)。
**解决**:先用 `pr +files` 拿到文件清单,只对**与评分相关**的源码/测试文件按文件取 diff
```bash
# 1. 先取文件清单(轻量),用于 test_coverage 维度与体量判断
gitlink-cli pr +files -i <id> --format json
# 2. 对单个文件取差异(避免一次拉全量)
gitlink-cli pr +version-diff -i <id> -v <version-id> -f path/to/file.go --format json
# version-id 从 pr +versions -i <id> 获取patchset 版本)
```
> 注意:`test_coverage`、`pr_hygiene` 的体量判定只依赖**文件清单**`changed_src`/`changed_tests`/`changed_files`),不需要全量 diff只有 `review_findings`AI 审查)才需要看 diff 内容。所以分段时优先保证文件清单完整diff 可按文件懒加载。避免短时间内重复请求 files/diff 接口。
---
## 5. CI 状态取不到(按 `unknown` 处理)
**症状**:评分卡 `CI status` 行显示 `unknown`,得分为权重的一半(默认 `10 → 5`)。
**原因**`gitlink-cli ci +builds` 返回的是仓库的构建列表,并不保证能定位到**正好对应当前 PR 头部 commit** 的那次构建——仓库可能没配 CI、构建尚未触发、或无法把构建与该 PR 的 commit 关联。此时 CI 既非「通过」也非「失败」,而是**未知**。
**原因细节与评分映射**(见 `REFERENCE.md` 3.5
| CI 情况 | ci_status 得分(权重 10 时) |
|---------|:---:|
| 找到对应构建且通过 | 10 |
| 找到对应构建且失败 | 0 |
| 取不到 / 无 CI / 无法关联 | 5`round(10 * 0.5)` |
**解决**
- 这是**预期的降级行为**,不是 bug取不到就当 `unknown`,给一半分,**不要**当成失败而误触发 `require_ci_pass` 硬门禁。
- 若希望 `unknown` 也拦截,可在策略里收紧(但要清楚这会拦下没配 CI 的仓库)。默认 `require_ci_pass: true` 的语义是「**CI 明确失败**才拦」,`unknown` 不触发硬门禁。
```bash
# 排查:先看仓库到底有没有构建记录
gitlink-cli ci +builds --limit 20 --format json
```
> 注意:硬门禁 `require_ci_pass` 只在 CI **明确失败**时命中;`unknown` 不算失败、不触发硬门禁,仅按 0.5×权重计分。
---
## 6. 如何表达 REQUEST_CHANGES 裁决(为何用评论而非 rejected
**症状**:纠结要不要用 GitLink review 的 `rejected` 状态把 gatekeeper 的 `REQUEST_CHANGES` 裁决"硬"标到 PR 上。
**原因 / 设计选择**GitLink 原生 PR review 的 `status``common`/`approved`/`rejected``rejected` 即"请求修改"的等价)。但 `approved`/`rejected` 是**强语义的人工授意动作**。gatekeeper 是自动门禁,**刻意不替人按下 approved/rejected** —— 所有自动裁决(含 REQUEST_CHANGES一律以**建议性的 `common` 评论**回写,把强语义留给维护者。
**解决**:用「评论 + 标签」组合表达裁决(这正是本作品复用 `label` 命令的原因):
1. 评分卡以 `pr +comment`(或 `pr +review --status common`)回写,**标题里明确标注裁决**`Verdict: ❌ REQUEST_CHANGES`),一眼可读;
2. 同时打上状态标签 `gatekeeper:needs-changes`,让状态可被列表/过滤识别。
```bash
# 评分卡回写(标题已含裁决,正文是评分卡)—— 与 workflow 脚本一致,走 pr +comment
gitlink-cli pr +comment -i <id> -b "$(cat scorecard.md)"
# 也可作为评审记录gitlink-cli pr +review -i <id> --status common --content "$(cat scorecard.md)"
```
> 注意gatekeeper **绝不**自动发 `approved`/`rejected` —— 那是维护者的权限。裁决的「拦截」语义靠标题文字 + `gatekeeper:needs-changes` 标签承载。
---
## 7. `label` 不存在,需先 `create`
**症状**:打标签时提示标签不存在,或 `issue_tag_ids` 里传了一个查不到的 ID。
**原因**`gatekeeper.yaml` 里配置的 `labels``gatekeeper:pass` / `gatekeeper:needs-changes` / `gatekeeper:review`)只是名字,仓库里**未必已经创建**对应的 label 实体。给 PR/Issue 挂标签时用的是 label 的**数字 ID**,名字对不上数据库里就没有,自然挂不上。
**解决**:先查后建——先用 `label +list` 看标签是否存在并拿到 ID缺的用 `label +create` 创建(注意 `label` 组**只有** `+list/+create/+update/+delete`**没有** `+view`
```bash
# 1. 查标签是否已存在,拿 id--only-name true 只返回 id+name便于解析
gitlink-cli label +list --keyword gatekeeper --only-name true --format json
# 2. 缺失则创建color 为十六进制,缺省有内置默认色)
gitlink-cli label +create -n "gatekeeper:needs-changes" -d "Gatekeeper requested changes" -c "#D73A4A" --format json
gitlink-cli label +create -n "gatekeeper:pass" -d "Gatekeeper passed" -c "#0E8A16" --format json
gitlink-cli label +create -n "gatekeeper:review" -d "Gatekeeper left comments" -c "#FBCA04" --format json
# 3. 把标签 id 挂到 PR 背后的 issue用 PR 关联的 issue.id非 PR 号;先取:)
ISSUE_ID=$(gitlink-cli pr +view -i <pr> --format json | jq -r '.data.issue.id')
# 更新时需带 done_ratio/subject/description见 gitlink-shared
gitlink-cli api POST /:owner/:repo/issues/$ISSUE_ID --body '{
"issue_tag_ids": [<tag_id>],
"done_ratio": 0,
"subject": "<原始标题>",
"description": "<原始描述>"
}'
# 也可用 gitlink-cli issue +update它会自动保留 subject/description免手动回传。
```
> 注意:标签是**幂等创建**——重复 `+create` 同名标签前应先 `+list` 检查,避免产生重名标签。更新 issue 挂标签时务必带上当前 `subject`/`description`,否则可能清空描述(`gitlink-shared` 已警示)。
---
## 8. `auto_merge` 想生效但没合并(需三者同时满足)
**症状**:策略里写了 `auto_merge: true`,跑完却没有合并 PR。
**原因**:这是**故意的安全设计**,不是 bug。gatekeeper 绝不轻易合并,合并必须**三个条件同时成立**
| 条件 | 来源 | 缺了会怎样 |
|------|------|-----------|
| `verdict == PASS` | 评分结果 | 非 PASSCOMMENT/REQUEST_CHANGES一律不合并 |
| 命令带 `--apply` | 运行参数 | 不带则全程 dry-run只打印不写不合并 |
| 策略 `auto_merge: true` | `gatekeeper.yaml` | 默认 `false`,不会合并 |
三者缺一不可。最常见的是**忘了 `--apply`**(默认 dry-run或裁决其实不是 PASS。
**解决**
```bash
# 先 dry-run 看裁决是不是 PASS默认就是 dry-run不写任何东西
gitlink-gatekeeper --pr <id>
# 确认 PASS 且策略 auto_merge: true 后,显式带 --apply 才会合并
gitlink-gatekeeper --pr <id> --apply
# 底层合并命令CLI flag 是 --method不是 --do详见 #12
gitlink-cli pr +merge -i <id> --method squash --format json
```
> 注意:`merge_method``merge`/`rebase`/`squash`)由策略 `behavior.merge_method` 决定,映射到 `pr +merge --method`。即便三条件都满足,合并前也应向用户复述「将以 `<method>` 合并 PR #<id>」。
---
## 9. 草稿draftPR
**症状**:对一个还是草稿状态的 PR 跑出了正式裁决;作者反馈「我还没写完」。
**原因**:草稿 PR 通常尚未完成描述未补、测试未加、commit 未整理),此时打门禁裁决意义不大,且容易误伤——`gitlink-code-review` 也建议对 draft PR 先提示作者转 Ready for Review。
**解决**:采集 PR 元信息后检查草稿标志(`pr +view` 返回里的 draft / WIP 标记,或标题含 `WIP`/`[Draft]`),命中则**不出裁决**,只提示:
```bash
gitlink-cli pr +view -i <id> --format json # 检查是否 draft / 标题含 WIP
```
> 注意:可在 dry-run 下对 draft PR 生成「预览评分卡」帮作者自查,但**不回写评论、不打标签、不合并**,直到 PR 转为 Ready for Review。
---
## 10. 分页 / 大仓库(列表只看到一页)
**症状**:大仓库里 `pr +list` / `ci +builds` / `label +list` 只返回 20 条,漏掉了你要找的 PR、构建或标签。
**原因**:列表类接口默认分页,`limit` 默认 20、`page` 默认 1。返回 Envelope 的 `meta` 里有 `page`/`limit`/`total_count`,但**不会自动翻页**。
**解决**:按 `meta.total_count` 判断是否还有下一页,循环翻页直到取全:
```bash
# 第一页,先看 meta.total_count
gitlink-cli pr +list --state open --page 1 --limit 50 --format json
# 还有更多则继续翻页
gitlink-cli pr +list --state open --page 2 --limit 50 --format json
# 构建、标签同理
gitlink-cli ci +builds --page 1 --limit 50 --format json
gitlink-cli label +list --page 1 --limit 50 --format json
```
> 注意:把 `limit` 适当调大(如 50可减少请求轮次但 files/diff 接口有频率限制,翻页时不要过于密集。评分只针对**单个 PR**,分页主要用于「先在列表里定位到目标 PR 号」这一步。
---
## 11. 用 `--state` 过滤 PR 不准
**症状**`gitlink-cli pr +list --state open` 返回的列表里混进了已合并 / 已关闭的 PR。
**原因**GitLink 的真实行为(`gitlink-shared` 已记录)——`--state` 参数**仅影响统计计数**,返回的列表可能包含所有状态。不能只信 `--state`
**解决**:在客户端用每条 PR 的 `pull_request_status` 字段二次过滤:
| `pull_request_status` | 含义 |
|:---:|------|
| `0` | open |
| `1` | merged |
| `2` | closed |
gatekeeper 只对 `pull_request_status == 0`open的 PR 做裁决;对已 merged/closed 的应跳过并提示。
> 注意:这是平台行为,不是 CLI bug。任何依赖「PR 是否仍 open」的逻辑如批量门禁扫描都必须以 `pull_request_status` 为准,而非 `--state`
---
## 12. `pr +merge` 的 flag 是 `--method`,不是 `--do`
**症状**:执行 `gitlink-cli pr +merge -i <id> --do squash` 报错「未知 flag `--do`」。
**原因**`do` 是 GitLink 合并 API`POST .../pulls/:id/pr_merge`)的**底层请求字段名**;而 `gitlink-cli` 暴露给用户的 **flag 名是 `--method`(短选项 `-m`**,默认值 `merge`。两者不要混淆——文档/笔记里若看到 `--do` 是记错了。
**解决**
```bash
# 正确:用 --method / -m
gitlink-cli pr +merge -i <id> --method squash --format json
gitlink-cli pr +merge -i <id> -m rebase --format json
# 合并方式取值merge | rebase | squash缺省 merge
```
> 注意gatekeeper 的 `behavior.merge_method` 直接对应 `--method`。合并仍受 #8 的三条件约束PASS + `--apply` + `auto_merge: true`)。
---
## 还没解决?
1. 加 `--debug` 重跑,看原始请求/响应(`gitlink-cli ... --debug`)。
2. 用 `--format json` 拿结构化输出,核对 `error.code` / `error.suggestion` 与本文对照。
3. 回到 [`SKILL.md`](./SKILL.md) 重走工作流,确认每步命令与参数;字段/算法/阈值一律以 [`REFERENCE.md`](./REFERENCE.md)SSOT与 [`REFERENCE.md`](./REFERENCE.md) 为准。
4. 始终遵守安全默认:默认 dry-run、绝不自动合并、写操作前复述意图、不回显 Token。

View File

@ -0,0 +1,163 @@
# 裁决记录 — COMMENT总分介于两阈值之间无硬门禁失败
> 模拟 `gitlink-gatekeeper` 的完整执行记录:采集 → 逐维算分 → 硬门禁 → 裁决 → 渲染评分卡 → dry-run 回写命令。
> 策略:默认 `gatekeeper.yaml`(权重 40/20/15/15/10severity_penalty blocker=100/major=25/minor=5/nit=1thresholds pass=85 / request_changes=60max_changed_files=80
> 命令均来自 `REFERENCE.md` 第 7 节 CLI 映射,已对照真实 shortcut 核验。
**场景**`Gitlink/forgeplus` 仓库 PR #277,给配置加载器增加默认值合并能力,改动 2 个源码文件,附 1 个测试文件CI 通过AI 审查只发现若干 minor/nit无 blocker、无 major。总分落在 6085 之间 → COMMENT建议性意见不阻塞、不批准
---
## ① 采集上下文
```bash
gitlink-cli pr +view -i 277 --owner Gitlink --repo forgeplus --format json
gitlink-cli pr +files -i 277 --owner Gitlink --repo forgeplus --format json
gitlink-cli pr +diff -i 277 --owner Gitlink --repo forgeplus --format json
gitlink-cli api GET /Gitlink/forgeplus/pulls/277/commits --format json
gitlink-cli ci +builds --owner Gitlink --repo forgeplus --format json
```
**采集结果摘要**
| 信号 | 值 |
|------|----|
| 标题 | `feat(config): merge defaults on load` |
| 描述 | 非空,长度 52 字符≥30**无** `#<n>` 关联 |
| `changed_files` | 2 |
| `changed_src` | 2`loader.go` / `merge.go` |
| `changed_tests` | 1`merge_test.go` |
| commits | 3 条,符合 Conventional Commits 的 2 条 → conforming=2 |
| CI | passing |
| AI 审查发现 | 0 blocker / 0 major / 3 minor / 2 nit |
---
## ② 逐维算分(公式代入,可复算)
**review_findings权重 40** — §3.1
```
penalty = 3×5(minor) + 2×1(nit) = 17
score = 40 × max(0, 1 17/40) = 40 × (23/40) = 40 × 0.575 = 23
```
**test_coverage权重 20** — §3.2
```
changed_src=2 > 0, changed_tests=1 > 0
ratio = min(1, 1/2) = 0.5
score = round(20 × (0.5 + 0.5×0.5)) = round(20 × 0.75) = 15
```
**pr_hygiene权重 15** — §3.3
```
描述≥30 ✓ (+1/3)
关联 Issue ✗ (0)
体量 changed_files=2 ≤ max_changed_files/2 = 40 ✓ (+1/3)
命中比例 = 2/3
score = round(15 × 2/3) = round(10.0) = 10
```
**commit_quality权重 15** — §3.4
```
conforming/total = 2/3
score = round(15 × 2/3) = round(10.0) = 10
```
**ci_status权重 10** — §3.5
```
CI passing → score = 10
```
**总分**
```
total = 23 + 15 + 10 + 10 + 10 = 68
```
---
## ③ 硬门禁判定§4
| 门禁 | 配置 | 命中? |
|------|------|:------:|
| `forbid_blocker_findings` | true | 否0 blocker |
| `require_ci_pass` | true | 否passing |
| `require_tests_for_src_changes` | true | 否changed_src=2>0 且 changed_tests=1>0 |
| `require_linked_issue` | false | —(未启用) |
| `max_changed_files` | 80 | 否2 ≤ 80 |
`hard_gate_failed = false`
---
## ④ 裁决§5
```
hard_gate_failed = false
total = 68not (≥ pass 85)not (< request_changes 60)
→ 介于两阈值之间 → verdict = COMMENT
```
---
## ⑤ 渲染的评分卡
```markdown
## 🛡️ Gatekeeper Report — PR #277 feat(config): merge defaults on load
**Verdict: 💬 COMMENT** · Score: 68/100 · policy: gatekeeper.yaml@v1
| Dimension | Weight | Score | Notes |
|-----------|:------:|:-----:|-------|
| Review findings | 40 | 23/40 | 0 blocker / 0 major / 3 minor / 2 nit |
| Test coverage | 20 | 15/20 | 2 src / 1 test files |
| PR hygiene | 15 | 10/15 | desc ✓ / linked issue ✗ / size ✓ |
| Commit quality | 15 | 10/15 | 2/3 conventional |
| CI status | 10 | 10/10 | passing |
### 🟡 Should fix (3)
- [minor] `mergeDefaults` 对 nil map 未保护,可能 panic — merge.go:33
- [minor] 深拷贝缺失,合并后仍共享底层 slice — merge.go:58
- [minor] 加载失败时未记录原始路径,定位困难 — loader.go:21
### 🔵 Nits (2)
- [nit] 导出函数 `Merge` 缺少 doc 注释 — merge.go:18
- [nit] 一条 commit 未用 `type(scope):` 前缀 — (commit)
### ✅ Strengths
- 改动小而聚焦附带了测试CI 全绿
- 无 blocker、无 major整体方向正确
### Next steps
1. 处理 3 条 minor尤其 nil map 与共享 slice并补充关联 Issue 编号
2. 修复后总分有望跨过 pass 阈值85转为 PASS当前为非阻塞的 COMMENT维护者可酌情合并
---
*Generated by gitlink-gatekeeper · policy-as-code PR gate · re-run after changes*
```
---
## ⑥ 将执行的回写命令dry-run 展示)
> 默认 dry-run不传 `--apply` 时仅打印。COMMENT 裁决不阻塞、不批准、不合并。
```bash
# [dry-run] 1) 评分卡作为评论回写(与 workflow 脚本一致,走 pr +comment
gitlink-cli pr +comment -i 277 -b "$(cat scorecard.md)" --owner Gitlink --repo forgeplus
# 也可作为评审记录gitlink-cli pr +review -i 277 --status common --content "$(cat scorecard.md)"
# [dry-run] 2) 打 review 标签labels.comment = "gatekeeper:review"
gitlink-cli label +create -n "gatekeeper:review" -c "#0969DA" \
-d "Gatekeeper verdict: COMMENT" --owner Gitlink --repo forgeplus
# 标签挂到 PR 背后的 issue.id非 PR 号):
ISSUE_ID=$(gitlink-cli pr +view -i 277 --owner Gitlink --repo forgeplus --format json | jq -r '.data.issue.id')
gitlink-cli api POST /Gitlink/forgeplus/issues/$ISSUE_ID --body '{
"issue_tag_ids": [<gatekeeper:review tag_id>],
"done_ratio": 0,
"subject": "<PR 原标题>",
"description": "<PR 原描述>"
}'
# COMMENT 不合并、不创建 tracking issue仅 REQUEST_CHANGES 才善后建 issue
```
> 实际写回需追加 `--apply`Agent 会先复述「将回写 1 条评论 + 打 gatekeeper:review 标签,不合并」并等待确认。

View File

@ -0,0 +1,178 @@
# 裁决记录 — PASS
> 模拟 `gitlink-gatekeeper` 在一个真实风格 PR 上的完整执行记录:采集 → 逐维算分 → 硬门禁 → 裁决 → 渲染评分卡 → dry-run 回写命令。
> 策略:默认 `gatekeeper.yaml`(权重 40/20/15/15/10severity_penalty blocker=100/major=25/minor=5/nit=1thresholds pass=85 / request_changes=60max_changed_files=80
> 命令均来自 `REFERENCE.md` 第 7 节 CLI 映射,已对照 `gitlink-cli pr/ci/label` 真实 shortcut 核验。
**场景**`Gitlink/forgeplus` 仓库 PR #214给搜索模块增加分页参数校验附带单元测试CI 全绿。
---
## ① 采集上下文
```bash
# PR 元信息(标题/描述/作者/关联)
gitlink-cli pr +view -i 214 --owner Gitlink --repo forgeplus --format json
# 变更文件列表
gitlink-cli pr +files -i 214 --owner Gitlink --repo forgeplus --format json
# diff供 AI 审查)
gitlink-cli pr +diff -i 214 --owner Gitlink --repo forgeplus --format json
# commits
gitlink-cli api GET /Gitlink/forgeplus/pulls/214/commits --format json
# CI 状态
gitlink-cli ci +builds --owner Gitlink --repo forgeplus --format json
```
**采集结果摘要**
| 信号 | 值 |
|------|----|
| 标题 | `feat(search): validate pagination params` |
| 描述 | 非空,长度 142 字符≥30且含 `Closes #198` |
| `changed_files` | 5 |
| `changed_src`(匹配 `source_globs` | 3`search.go` / `paginate.go` / `params.go` |
| `changed_tests`(匹配 `test_globs` | 2`paginate_test.go` / `params_test.go` |
| commits | 4 条,全部 `type(scope): subject` 形式 → conforming=4 |
| CI | passing |
| AI 审查发现 | 0 blocker / 0 major / 1 minor / 2 nit |
---
## ② 逐维算分(公式代入,可复算)
**review_findings权重 40** — `REFERENCE.md` §3.1
```
penalty = 1×5(minor) + 2×1(nit) = 7
score = 40 × max(0, 1 7/40) = 40 × (33/40) = 40 × 0.825 = 33
```
**test_coverage权重 20** — §3.2
```
changed_src=3 > 0, changed_tests=2 > 0
ratio = min(1, 2/3) = 0.6667
score = round(20 × (0.5 + 0.5×0.6667)) = round(20 × 0.8333) = round(16.667) = 17
```
**pr_hygiene权重 15** — §3.3
```
描述≥30 ✓ (+1/3)
关联 Issuebody 含 #198 ✓ (+1/3)
体量 changed_files=5 ≤ max_changed_files/2 = 40 ✓ (+1/3)
命中比例 = 3/3 = 1
score = round(15 × 1) = 15
```
**commit_quality权重 15** — §3.4
```
conforming/total = 4/4
score = round(15 × 4/4) = 15
```
**ci_status权重 10** — §3.5
```
CI passing → score = 10
```
**总分**
```
total = 33 + 17 + 15 + 15 + 10 = 90
```
---
## ③ 硬门禁判定§4
| 门禁 | 配置 | 命中? |
|------|------|:------:|
| `forbid_blocker_findings` | true | 否0 blocker |
| `require_ci_pass` | true | 否passing |
| `require_tests_for_src_changes` | true | 否changed_src=3>0 但 changed_tests=2>0 |
| `require_linked_issue` | false | —(未启用) |
| `max_changed_files` | 80 | 否5 ≤ 80 |
`hard_gate_failed = false`
---
## ④ 裁决§5
```
hard_gate_failed = false
total = 90 ≥ thresholds.pass(85)
→ verdict = PASS
```
---
## ⑤ 渲染的评分卡
```markdown
## 🛡️ Gatekeeper Report — PR #214 feat(search): validate pagination params
**Verdict: ✅ PASS** · Score: 90/100 · policy: gatekeeper.yaml@v1
| Dimension | Weight | Score | Notes |
|-----------|:------:|:-----:|-------|
| Review findings | 40 | 33/40 | 0 blocker / 0 major / 1 minor / 2 nit |
| Test coverage | 20 | 17/20 | 3 src / 2 test files |
| PR hygiene | 15 | 15/15 | desc ✓ / linked issue #198 ✓ / size ✓ |
| Commit quality | 15 | 15/15 | 4/4 conventional |
| CI status | 10 | 10/10 | passing |
### 🟡 Should fix (1)
- [minor] `validatePage` 对负数与零分两个分支处理,可合并 — paginate.go:41
### 🔵 Nits (2)
- [nit] 导出函数 `ValidateParams` 缺少 doc 注释 — params.go:18
- [nit] 测试用例命名建议统一为 `TestXxx_Case` — params_test.go:7
### ✅ Strengths
- 源码改动均有对应测试,覆盖良好
- commit 全部符合 Conventional Commits关联了 Issue #198
- CI 全绿
### Next steps
1. 评分达标且无硬门禁失败,可合并;如需自动合并请在策略中开启 `auto_merge` 并显式带 `--apply`
2. 两个 nit 可在后续提交一并处理,不阻塞合并
---
*Generated by gitlink-gatekeeper · policy-as-code PR gate · re-run after changes*
```
---
## ⑥ 将执行的回写命令dry-run 展示)
> 默认 `behavior.dry_run_default: true` → 不传 `--apply` 时只打印以下命令,不真正回写。
> `behavior.auto_merge: false` → 即便裁决 PASS 也**不会**触发合并。
```bash
# [dry-run] 1) 把评分卡作为评论回写到 PR与 workflow 脚本一致,走 pr +comment
gitlink-cli pr +comment -i 214 -b "$(cat scorecard.md)" --owner Gitlink --repo forgeplus
# 也可作为评审记录回写gitlink-cli pr +review -i 214 --status common --content "$(cat scorecard.md)"
# GitLink review status 为 common/approved/rejectedgatekeeper 自动裁决统一用建议性的 common
# 强语义 approved/rejected 留给人工授意。)
# [dry-run] 2) 打 PASS 标签labels.pass = "gatekeeper:pass"
# 2a. 确保标签存在(首次需创建;已存在则跳过)
gitlink-cli label +create -n "gatekeeper:pass" -c "#2EA043" \
-d "Gatekeeper verdict: PASS" --owner Gitlink --repo forgeplus
# 2b. 标签挂到 PR 背后的 issueGitLink PR 复用 issue 标签体系)。
# 注意:用 PR 关联的 issue.id非 PR 号),先从 pr +view 取:
ISSUE_ID=$(gitlink-cli pr +view -i 214 --owner Gitlink --repo forgeplus --format json | jq -r '.data.issue.id')
gitlink-cli api POST /Gitlink/forgeplus/issues/$ISSUE_ID --body '{
"issue_tag_ids": [<gatekeeper:pass tag_id>],
"done_ratio": 0,
"subject": "<PR 原标题>",
"description": "<PR 原描述>"
}'
# 也可用 gitlink-cli issue +update它会自动保留 subject/description免手动回传。
# 合并默认禁用auto_merge=false。仅当策略 auto_merge=true 且裁决=PASS 且显式 --apply 时才会执行:
# gitlink-cli pr +merge -i 214 --method squash --owner Gitlink --repo forgeplus
```
> 实际写回需追加 `--apply`,且 Agent 会先向用户复述「将回写 1 条评论 + 打 1 个 gatekeeper:pass 标签,不合并」并等待确认。

View File

@ -0,0 +1,169 @@
# 裁决记录 — REQUEST_CHANGES硬门禁失败 + 1 个 major
> 模拟 `gitlink-gatekeeper` 的完整执行记录:采集 → 逐维算分 → 硬门禁 → 裁决 → 渲染评分卡 → dry-run 回写命令。
> 策略:默认 `gatekeeper.yaml`(权重 40/20/15/15/10severity_penalty blocker=100/major=25/minor=5/nit=1thresholds pass=85 / request_changes=60max_changed_files=80
> 命令均来自 `REFERENCE.md` 第 7 节 CLI 映射,已对照真实 shortcut 核验。
**场景**`Gitlink/forgeplus` 仓库 PR #305,重构订单结算逻辑,改动了 4 个源码文件但**未附带任何测试**其中一处折扣计算有边界缺陷major。CI 通过。
---
## ① 采集上下文
```bash
gitlink-cli pr +view -i 305 --owner Gitlink --repo forgeplus --format json
gitlink-cli pr +files -i 305 --owner Gitlink --repo forgeplus --format json
gitlink-cli pr +diff -i 305 --owner Gitlink --repo forgeplus --format json
gitlink-cli api GET /Gitlink/forgeplus/pulls/305/commits --format json
gitlink-cli ci +builds --owner Gitlink --repo forgeplus --format json
```
**采集结果摘要**
| 信号 | 值 |
|------|----|
| 标题 | `refactor(billing): rework settlement pipeline` |
| 描述 | 非空,长度 88 字符≥30**无** `#<n>` 关联 |
| `changed_files` | 4 |
| `changed_src` | 4`settlement.go` / `discount.go` / `invoice.go` / `tax.go` |
| `changed_tests` | 0 |
| commits | 3 条,符合 Conventional Commits 的 2 条1 条为 `wip: fix`)→ conforming=2 |
| CI | passing |
| AI 审查发现 | 0 blocker / 1 major / 1 minor / 2 nit |
---
## ② 逐维算分(公式代入,可复算)
**review_findings权重 40** — §3.1
```
penalty = 25(major) + 1×5(minor) + 2×1(nit) = 32
score = 40 × max(0, 1 32/40) = 40 × (1 0.8) = 40 × 0.2 = 8
```
**test_coverage权重 20** — §3.2
```
changed_src=4 > 0, changed_tests=0
→ score = 0
```
**pr_hygiene权重 15** — §3.3
```
描述≥30 ✓ (+1/3)
关联 Issue ✗ (0)
体量 changed_files=4 ≤ max_changed_files/2 = 40 ✓ (+1/3)
命中比例 = 2/3
score = round(15 × 2/3) = round(10.0) = 10
```
**commit_quality权重 15** — §3.4
```
conforming/total = 2/3
score = round(15 × 2/3) = round(10.0) = 10
```
**ci_status权重 10** — §3.5
```
CI passing → score = 10
```
**总分**
```
total = 8 + 0 + 10 + 10 + 10 = 38
```
---
## ③ 硬门禁判定§4
| 门禁 | 配置 | 命中? |
|------|------|:------:|
| `forbid_blocker_findings` | true | 否0 blocker |
| `require_ci_pass` | true | 否passing |
| `require_tests_for_src_changes` | true | **是**changed_src=4>0 且 changed_tests=0 |
| `require_linked_issue` | false | —(未启用) |
| `max_changed_files` | 80 | 否4 ≤ 80 |
`hard_gate_failed = true`
---
## ④ 裁决§5
```
hard_gate_failed = true
→ verdict = REQUEST_CHANGES (无视总分;本例 total=38 < request_changes(60) 也指向 REQUEST_CHANGES结论一致
```
---
## ⑤ 渲染的评分卡
```markdown
## 🛡️ Gatekeeper Report — PR #305 refactor(billing): rework settlement pipeline
**Verdict: ❌ REQUEST_CHANGES** · Score: 38/100 · policy: gatekeeper.yaml@v1
| Dimension | Weight | Score | Notes |
|-----------|:------:|:-----:|-------|
| Review findings | 40 | 8/40 | 0 blocker / 1 major / 1 minor / 2 nit |
| Test coverage | 20 | 0/20 | 4 src / 0 test files |
| PR hygiene | 15 | 10/15 | desc ✓ / linked issue ✗ / size ✓ |
| Commit quality | 15 | 10/15 | 2/3 conventional |
| CI status | 10 | 10/10 | passing |
### ⛔ Hard gate failures (1)
- `require_tests_for_src_changes`: 改动了 4 个源码文件,但本 PR 未包含任何测试文件
### 🔴 Must fix (1)
- [major] 折扣金额未做下限保护,满减叠加时可算出负数总价 — discount.go:72
### 🟡 Should fix (1)
- [minor] `settle()` 吞掉了 tax 计算的 error应向上传播 — settlement.go:118
### 🔵 Nits (2)
- [nit] commit `wip: fix` 不符合 Conventional Commits建议 rebase 整理 — (commit)
- [nit] 导出结构体 `Invoice` 字段缺少注释 — invoice.go:14
### ✅ Strengths
- 改动聚焦、体量适中4 文件CI 全绿
### Next steps
1. **补测试**(最高优先级):为 4 个改动源码文件补单元测试,解除 `require_tests_for_src_changes` 硬门禁
2. 修复折扣下限的 major 缺陷,建议补一条针对负数总价的回归测试
3. 整理 `wip: fix` commit、补充 error 传播后,重新触发 gatekeeper
---
*Generated by gitlink-gatekeeper · policy-as-code PR gate · re-run after changes*
```
---
## ⑥ 将执行的回写命令dry-run 展示)
> 默认 dry-run不传 `--apply` 时仅打印。REQUEST_CHANGES 永不涉及合并。
```bash
# [dry-run] 1) 评分卡作为评论回写(与 workflow 脚本一致,走 pr +comment
gitlink-cli pr +comment -i 305 -b "$(cat scorecard.md)" --owner Gitlink --repo forgeplus
# 裁决=REQUEST_CHANGES 不直接用平台的强语义 rejected留给人工裁决靠评分卡标题 + 标签表达
# [dry-run] 2) 打 needs-changes 标签labels.request_changes = "gatekeeper:needs-changes"
gitlink-cli label +create -n "gatekeeper:needs-changes" -c "#D1242F" \
-d "Gatekeeper verdict: REQUEST_CHANGES" --owner Gitlink --repo forgeplus
# 标签挂到 PR 背后的 issue.id非 PR 号):
ISSUE_ID=$(gitlink-cli pr +view -i 305 --owner Gitlink --repo forgeplus --format json | jq -r '.data.issue.id')
gitlink-cli api POST /Gitlink/forgeplus/issues/$ISSUE_ID --body '{
"issue_tag_ids": [<gatekeeper:needs-changes tag_id>],
"done_ratio": 0,
"subject": "<PR 原标题>",
"description": "<PR 原描述>"
}'
# [dry-run] 3)(子题三善后)创建 tracking issue 汇总必修项并关联 PR #305
# 注意:用 $'...' 让 \n 成为真实换行(双引号里的字面 \n 不会换行)
gitlink-cli issue +create --owner Gitlink --repo forgeplus \
-t "[gatekeeper] PR #305 必修项跟踪" \
-b $'由 gatekeeper 裁决 REQUEST_CHANGES 触发。\n硬门禁: require_tests_for_src_changes。\nMust fix: discount.go:72 折扣下限。\n关联 PR: #305'
```
> 实际写回需追加 `--apply`Agent 会先复述「将回写 1 条评论 + 打 gatekeeper:needs-changes 标签 + 建 1 条 tracking issue不合并」并等待确认。

View File

@ -0,0 +1,50 @@
# gatekeeper.lenient.yaml — 宽松预设(低门槛,适合实验仓 / 早期项目 / 鼓励贡献的社区)
version: 1 # 策略 schema 版本,整数,当前为 1
# —— 加权评分维度5 项权重之和必须 = 100 ——
weights:
review_findings: 40 # AI 代码审查发现(按严重度扣分)
test_coverage: 20 # 改动的源码是否伴随测试
pr_hygiene: 15 # PR 描述 / 关联 Issue / 体量
commit_quality: 15 # commit 是否符合 Conventional Commits
ci_status: 10 # CI 是否通过
# —— 硬门禁只保留安全底线blocker / CI放开测试与 Issue 要求 ——
hard_gates:
forbid_blocker_findings: true # 出现 blocker 级发现即拦截(安全底线,保留)
require_ci_pass: true # CI 未通过即拦截(安全底线,保留)
require_tests_for_src_changes: false # 宽松:改源码不强制配测试
require_linked_issue: false # 宽松:不强制关联 Issue
max_changed_files: 200 # 宽松:放宽到 200 个文件
# —— 严重度 → 扣分(用于 review_findings 维度)——
severity_penalty:
blocker: 100 # 单条即清零本维度,且触发硬门禁
major: 25
minor: 5
nit: 1
# —— 最终 0-100 总分 → 裁决阈值(整体下移,更容易 PASS——
thresholds:
pass: 70 # 总分 ≥ 70 且无硬门禁失败 → PASS
request_changes: 45 # 总分 < 45 → REQUEST_CHANGES
# 介于两者之间 → COMMENT
# —— 各裁决对应回写的标签(依赖 gitlink-cli label——
labels:
pass: "gatekeeper:pass"
request_changes: "gatekeeper:needs-changes"
comment: "gatekeeper:review"
# —— 源码判定:用于 test_coverage / require_tests_for_src_changes ——
source_globs: ["**/*.go", "**/*.py", "**/*.js", "**/*.ts", "**/*.rs", "**/*.java"]
test_globs: ["**/*_test.go", "**/test_*.py", "**/*.test.*", "**/*.spec.*", "tests/**"]
# —— 行为开关 ——
behavior:
dry_run_default: true # 默认只预览,不写任何东西
post_comment: true # 把评分卡作为评论回写 PR
apply_label: true # 按裁决打标签
auto_merge: false # 仅当 true 且裁决=PASS 且显式 --apply 时才合并
merge_method: squash # merge | rebase | squash

View File

@ -0,0 +1,50 @@
# gatekeeper.strict.yaml — 严格预设(高合并门槛,适合核心库 / 稳定分支 / 严格 review 团队)
version: 1 # 策略 schema 版本,整数,当前为 1
# —— 加权评分维度5 项权重之和必须 = 100 ——
weights:
review_findings: 40 # AI 代码审查发现(按严重度扣分)
test_coverage: 20 # 改动的源码是否伴随测试
pr_hygiene: 15 # PR 描述 / 关联 Issue / 体量
commit_quality: 15 # commit 是否符合 Conventional Commits
ci_status: 10 # CI 是否通过
# —— 硬门禁:全部开启,命中任一 → 直接 REQUEST_CHANGES无视总分——
hard_gates:
forbid_blocker_findings: true # 出现 blocker 级发现即拦截
require_ci_pass: true # CI 未通过即拦截
require_tests_for_src_changes: true # 改了源码却没加测试即拦截
require_linked_issue: true # 严格PR 必须关联 Issue
max_changed_files: 40 # 严格PR 体量收紧到 40 个文件
# —— 严重度 → 扣分(用于 review_findings 维度)——
severity_penalty:
blocker: 100 # 单条即清零本维度,且触发硬门禁
major: 25
minor: 5
nit: 1
# —— 最终 0-100 总分 → 裁决阈值(整体上移,更难 PASS、更易拦截——
thresholds:
pass: 92 # 总分 ≥ 92 且无硬门禁失败 → PASS
request_changes: 75 # 总分 < 75 → REQUEST_CHANGES
# 介于两者之间 → COMMENT
# —— 各裁决对应回写的标签(依赖 gitlink-cli label——
labels:
pass: "gatekeeper:pass"
request_changes: "gatekeeper:needs-changes"
comment: "gatekeeper:review"
# —— 源码判定:用于 test_coverage / require_tests_for_src_changes ——
source_globs: ["**/*.go", "**/*.py", "**/*.js", "**/*.ts", "**/*.rs", "**/*.java"]
test_globs: ["**/*_test.go", "**/test_*.py", "**/*.test.*", "**/*.spec.*", "tests/**"]
# —— 行为开关 ——
behavior:
dry_run_default: true # 默认只预览,不写任何东西
post_comment: true # 把评分卡作为评论回写 PR
apply_label: true # 按裁决打标签
auto_merge: false # 仅当 true 且裁决=PASS 且显式 --apply 时才合并
merge_method: squash # merge | rebase | squash

View File

@ -0,0 +1,50 @@
# gatekeeper.yaml — 默认策略(均衡基线,等同 SSOT 第2节内置默认策略
version: 1 # 策略 schema 版本,整数,当前为 1
# —— 加权评分维度5 项权重之和必须 = 100 ——
weights:
review_findings: 40 # AI 代码审查发现(按严重度扣分)
test_coverage: 20 # 改动的源码是否伴随测试
pr_hygiene: 15 # PR 描述 / 关联 Issue / 体量
commit_quality: 15 # commit 是否符合 Conventional Commits
ci_status: 10 # CI 是否通过
# —— 硬门禁:任一命中 → 直接 REQUEST_CHANGES无视总分——
hard_gates:
forbid_blocker_findings: true # 出现 blocker 级发现即拦截
require_ci_pass: true # CI 未通过即拦截
require_tests_for_src_changes: true # 改了源码却没加测试即拦截
require_linked_issue: false # PR 是否必须关联 Issue
max_changed_files: 80 # 超过该改动文件数即拦截0 表示不限)
# —— 严重度 → 扣分(用于 review_findings 维度)——
severity_penalty:
blocker: 100 # 单条即清零本维度,且触发硬门禁
major: 25
minor: 5
nit: 1
# —— 最终 0-100 总分 → 裁决阈值 ——
thresholds:
pass: 85 # 总分 ≥ pass 且无硬门禁失败 → PASS
request_changes: 60 # 总分 < request_changes → REQUEST_CHANGES
# 介于两者之间 → COMMENT
# —— 各裁决对应回写的标签(依赖 gitlink-cli label——
labels:
pass: "gatekeeper:pass"
request_changes: "gatekeeper:needs-changes"
comment: "gatekeeper:review"
# —— 源码判定:用于 test_coverage / require_tests_for_src_changes ——
source_globs: ["**/*.go", "**/*.py", "**/*.js", "**/*.ts", "**/*.rs", "**/*.java"]
test_globs: ["**/*_test.go", "**/test_*.py", "**/*.test.*", "**/*.spec.*", "tests/**"]
# —— 行为开关 ——
behavior:
dry_run_default: true # 默认只预览,不写任何东西
post_comment: true # 把评分卡作为评论回写 PR
apply_label: true # 按裁决打标签
auto_merge: false # 仅当 true 且裁决=PASS 且显式 --apply 时才合并
merge_method: squash # merge | rebase | squash

View File

@ -0,0 +1,88 @@
# 评分卡样例scorecard-sample
> 这是 `gitlink-gatekeeper` 渲染到 PR 评论的 **评分卡** 的样例输出,严格套用 [`REFERENCE.md`](../REFERENCE.md) 第 6 节模板。
> 本样例对应一个 **REQUEST_CHANGES**含硬门禁失败案例用于展示模板的全部分区硬门禁、Must fix、Should fix、Nits、Strengths、Next steps 同时出现)。
> 数值来自默认策略 `gatekeeper.yaml`(权重 40/20/15/15/10blocker=100/major=25/minor=5/nit=1pass=85、request_changes=60可按第 35 节算法逐项复算。
---
## 输入快照(用于复算)
| 信号 | 值 |
|------|----|
| 变更文件总数 `changed_files` | 6 |
| 匹配 `source_globs` 的源码文件 `changed_src` | 4 |
| 匹配 `test_globs` 的测试文件 `changed_tests` | 0 |
| AI 审查发现 | 0 blocker / 1 major / 2 minor / 1 nit |
| PR 描述 | 非空,长度 64 字符≥30 |
| 关联 Issue | 否body 无 `#<n>` |
| commits | 共 4 条,符合 Conventional Commits 的 3 条 |
| CI 状态 | passing |
## 逐维复算
- **review_findings权重 40**`penalty = 25(major) + 2×5(minor) + 1(nit) = 36`
`score = 40 × max(0, 1 36/40) = 40 × 0.1 = 4`
- **test_coverage权重 20**`changed_src=4 > 0` 且 `changed_tests=0``score = 0`
- **pr_hygiene权重 15**描述≥30 ✓(+1/3);关联 Issue ✗(0);体量 `changed_files=6 ≤ max_changed_files/2 = 40` ✓(+1/3) → 命中比例 `2/3`
`score = round(15 × 2/3) = round(10.0) = 10`
- **commit_quality权重 15**`conforming/total = 3/4``score = round(15 × 3/4) = round(11.25) = 11`
- **ci_status权重 10**passing → `score = 10`
- **总分**`4 + 0 + 10 + 11 + 10 = 35`
## 硬门禁判定
| 门禁 | 配置 | 命中? |
|------|------|:------:|
| `forbid_blocker_findings` | true | 否(无 blocker |
| `require_ci_pass` | true | 否CI passing |
| `require_tests_for_src_changes` | true | **是**changed_src=4>0 且 changed_tests=0 |
| `require_linked_issue` | false | —(未启用) |
| `max_changed_files` | 80 | 否6 ≤ 80 |
`hard_gate_failed = true` → 裁决直接 **REQUEST_CHANGES**(无视总分;本例总分 35 < 60 亦指向 REQUEST_CHANGES结论一致)。
---
## 渲染的评分卡(回写到 PR 的正文)
```markdown
## 🛡️ Gatekeeper Report — PR #128 feat(auth): add refresh-token rotation
**Verdict: ❌ REQUEST_CHANGES** · Score: 35/100 · policy: gatekeeper.yaml@v1
| Dimension | Weight | Score | Notes |
|-----------|:------:|:-----:|-------|
| Review findings | 40 | 4/40 | 0 blocker / 1 major / 2 minor / 1 nit |
| Test coverage | 20 | 0/20 | 4 src / 0 test files |
| PR hygiene | 15 | 10/15 | desc ✓ / linked issue ✗ / size ✓ |
| Commit quality | 15 | 11/15 | 3/4 conventional |
| CI status | 10 | 10/10 | passing |
### ⛔ Hard gate failures (1)
- `require_tests_for_src_changes`: 改动了 4 个源码文件,但本 PR 未包含任何测试文件
### 🔴 Must fix (1)
- [major] 刷新令牌轮换未对旧令牌做失效处理,存在重放风险 — internal/auth/refresh.go:88
### 🟡 Should fix (2)
- [minor] `rotateToken` 缺少超时上下文,可能阻塞请求 — internal/auth/refresh.go:54
- [minor] 错误信息直接回显内部字段名,建议脱敏 — internal/auth/handler.go:131
### 🔵 Nits (1)
- [nit] 导出函数 `NewRotator` 缺少 doc 注释 — internal/auth/refresh.go:22
### ✅ Strengths
- commit 历史清晰3/4 符合 Conventional Commits
- CI 全绿PR 描述完整
### Next steps
1. **补测试**:为改动的 4 个源码文件添加单元测试,解除 `require_tests_for_src_changes` 硬门禁
2. 修复 major 重放风险后重新触发 gatekeeper
---
*Generated by gitlink-gatekeeper · policy-as-code PR gate · re-run after changes*
```
---
> **复算提示**:把上表「输入快照」的数值代入 `REFERENCE.md` 第 3 节公式即可重现每一维得分;硬门禁按第 4 节逐项比对;最终裁决按第 5 节判定树得出。同策略 + 同输入 → 同评分卡(确定性)。