feat(examples): add pr-quality-gatekeeper end-to-end workflow

Runnable reference implementation of the merged gitlink-gatekeeper Skill:
collect -> route -> decide (deterministic 0-100 scorecard, three-state verdict)
-> write-back (comment + label + tracking issue, --apply gated, never auto-merge).

- scripts/gatekeeper_workflow.py: single-PR gate loop (pure stdlib, py>=3.9)
- scripts/gatekeeper_sweep.py: repo-wide triage — real run over all 113 open PRs
  of this repo (read-only): PASS 105 / COMMENT 6 / REQUEST_CHANGES 2, avg 88.5
- findings.example.json: real code-review findings of a real PR diff (verifiable line numbers)
- ci-example/: Gitea Actions integration (exit code 2 = REQUEST_CHANGES as CI gate)
- docs: architecture / quickstart / runbook / verification (real-platform evidence)
- tests/test_scoring.py: determinism regression guard (green)

Closes the sub-task-3 loop on top of the label command (#89) and the
gitlink-gatekeeper Skill (#90), sharing one policy file across all three layers.
This commit is contained in:
何开元 2026-06-11 19:03:52 -07:00
parent 52b7093846
commit 85a67a875f
17 changed files with 2535 additions and 0 deletions

View File

@ -0,0 +1,3 @@
outputs/
__pycache__/
*.pyc

View File

@ -0,0 +1,69 @@
# PR 质量门禁工作流pr-quality-gatekeeper
把已收录的 [`gitlink-gatekeeper` Skill](../../../skills/gitlink-gatekeeper/SKILL.md)Policy-as-Code 合并门禁)包成**可直接运行的端到端工作流**
> **采集 → 路由 → 裁决 → 回写/善后**:读取一个真实 PR 的元信息/变更文件/commits/CI按变更路径建议 reviewer`gatekeeper.yaml` 策略算出**确定性 0100 评分卡**与**三态裁决**PASS / REQUEST_CHANGES / COMMENT仅在 `--apply`把评分卡评论、裁决标签、tracking issue 真实回写到 GitLink。
与仓库内已有能力的关系:`label` 命令(裁决标签)→ `gitlink-gatekeeper` Skill裁决知识**本工作流(可复现闭环)**,三层共用同一套策略文件,互为支撑而非重复。
## 交付物
- `scripts/gatekeeper_workflow.py`:单 PR 门禁闭环纯标准库Python ≥3.9,零第三方依赖)
- `scripts/gatekeeper_sweep.py`**仓库级批量体检**——对全部 open PR 逐个 dry-run产出治理报告
- `owner-rules.example.yaml`:变更路径 → reviewer 的路由表样例
- `config.example.yaml`:工作流配置样例(命令行参数可覆盖)
- `findings.example.json`AI/人工审查发现注入样例(**来自对真实 PR diff 的真实审查**,行号可复核)
- `docs/architecture.md` · `docs/quickstart.md` · `docs/runbook.md` · `docs/verification.md`
- `ci-example/`Gitea Actions 接入示例PR 触发自动门禁,退出码 2 = REQUEST_CHANGES
- `examples/demo-outputs/`真实平台运行产物PASS 90 评分卡 / 注入发现后的 55 分评分卡 / 113 个 open PR 的全仓体检报告)
- `tests/test_scoring.py`:确定性回归护栏(同输入 → 同分 → 同裁决)
## 快速运行(默认 dry-run不写远端
```bash
npm install -g @gitlink-ai/cli # ≥0.2.0,自带 label 命令与 gitlink-gatekeeper Skill
gitlink-cli auth login
python3 scripts/gatekeeper_workflow.py \
--owner <owner> --repo <repo> --pr <PR号> \
--policy ../../../skills/gitlink-gatekeeper/examples/gatekeeper.yaml \
--owner-rules owner-rules.example.yaml \
--output-dir outputs
```
- 注入审查发现得到含扣分的评分卡:加 `--findings findings.example.json`
- 真实回写(评论 + 标签 + tracking issue`--apply`(请先在自有仓库演练)
- 全仓批量体检(只读,零写入):
```bash
python3 scripts/gatekeeper_sweep.py \
--owner <owner> --repo <repo> \
--policy ../../../skills/gitlink-gatekeeper/examples/gatekeeper.yaml \
--owner-rules owner-rules.example.yaml \
--output-dir sweep-out --date-label $(date +%F)
```
更多见 [`docs/quickstart.md`](docs/quickstart.md) 与 [`docs/runbook.md`](docs/runbook.md)。
## 已在真实平台验证
全部证据见 [`docs/verification.md`](docs/verification.md),要点:
| 验证 | 对象 | 结果 |
|------|------|------|
| dry-run | 本仓库真实 PRpull_request_id 15222 | ✅ PASS 90/1008 个变更文件路由正确 |
| 注入真实审查发现 | 同一 PR + `findings.example.json` | ❌ REQUEST_CHANGES 55/100裁决翻转确定性可复算 |
| `--apply` 真实回写 | 自有 fork 的演练 PR | 评分卡评论 + tracking issue + 裁决标签全部由 API 回执确认 |
| **全仓批量体检** | 本仓库**全部 113 个 open PR** | 113/113 成功PASS 105 / COMMENT 6 / REQUEST_CHANGES 2均分 88.596% 未关联 issue |
| 单测 | `tests/test_scoring.py` | 全绿(锁定四个权威裁决案例的分值与裁决) |
## 设计要点
- **确定性评分**AI 只负责产出「发现列表」(可选注入),扣分与裁决由纯函数完成——同策略 + 同 PR → 同裁决,可逐位手算复现、可审计。
- **安全默认**:默认 dry-run 什么都不写;即便策略开了 `auto_merge`,也必须 `verdict == PASS` 且显式 `--apply` 才会合并;强语义的 approve/reject 始终留给人,自动裁决只以建议性 `common` 评论 + 标签呈现。
- **原生适配 GitLink**PR 标题/描述取自 `pr +view``issue.subject/description`;标签挂载走「`label +list` 查 id → Raw API `POST /:owner/:repo/issues/<issue_id>`」;尊重 `common/approved/rejected` 三态 review。
- **零依赖、零常驻**:纯标准库脚本 + `gitlink-cli`,无需部署 webhook 服务或数据库CI 一条 step 即可接入(见 `ci-example/`);确定性意味着**大规模治理零 AI 成本**。
## 许可证
随仓库 [MulanPSL-2.0](../../../LICENSE)。

View File

@ -0,0 +1,27 @@
# CI 集成示例 —— 门禁接 CI
本目录演示如何把 **gitlink-gatekeeper** 的 PR 看门人门禁接到 CI 上,让裁决直接挡住不达标的 PR。
> 这是**示例**,不是开箱即用的生产配置;`gitlink-cli` 的安装方式、PR 编号字段名需按你的 runner 实际情况调整。
## 文件
- [`gatekeeper.gitea.yml`](gatekeeper.gitea.yml)Gitea Actions 工作流GitLink 基于 Gitea语法与 GitHub Actions 兼容)。
## 用法
1. 把 `gatekeeper.gitea.yml` 复制到目标仓库的 `.gitea/workflows/` 目录。
2. 在仓库 **Settings → Actions → Secrets** 新增 `GITLINK_TOKEN`,值为有权读取该仓库 PR 的访问令牌(供 `gitlink-cli` 认证)。**Token 切勿写进仓库或日志。**
3. 提一个 PR 触发工作流即可。
## 工作原理
- 触发PR 的 `opened` / `synchronize` / `reopened` 事件。
- 步骤:检出 → 准备 Python 3.9(脚本纯标准库,无需装依赖)→ 装 `gitlink-cli` → 跑 `scripts/gatekeeper_workflow.py` 采集本次 PR 上下文并评分裁决。
- **退出码即门禁**
- `0` = PASS / COMMENT → job 通过,放行。
- `2` = REQUEST_CHANGES → 工作流把它转成 job 失败,挡住该 PR。
- `1` = 可预期错误(缺参数 / 未装 `gitlink-cli` 等)→ 同样失败。
- 产物:评分卡与 `summary.json` 落在 `outputs/`,工作流用 `upload-artifact` 上传,便于在 CI 页面查看裁决依据。
调门禁松紧只需改 `--policy` 指向的 `gatekeeper.yaml`(策略字段说明见 [`gitlink-gatekeeper` Skill REFERENCE](../../../../skills/gitlink-gatekeeper/REFERENCE.md))。

View File

@ -0,0 +1,75 @@
# gitlink-gatekeeper —— Gitea Actions CI 示例GitLink 平台用)
#
# ⚠️ 这是一个「门禁接 CI」的演示示例不是开箱即用的生产配置。
# - GitLink 基于 Gitea其 Actions 语法与 GitHub Actions 兼容,工作流放在
# 仓库的 .gitea/workflows/ 目录下。把本文件复制过去并按需调整即可启用。
# - 需要在仓库 Settings → Actions → Secrets 配置一个 GITLINK_TOKEN secret
# (供 gitlink-cli 认证、采集目标 PR 的上下文。Token 切勿写进仓库。
# - runner 需能访问 GitLink APIPython 3.9+ 与 gitlink-cli 的安装方式按实际
# runner 镜像调整(下面 install 步骤仅为占位示意)。
#
# 触发:对 PR 的 open / 同步事件运行门禁,脚本返回码 2REQUEST_CHANGES
# 会让本 job 失败,从而在 CI 上挡住该 PRPASS/COMMENT 返回 0 即通过)。
name: gatekeeper
on:
pull_request:
types: [opened, synchronize, reopened]
jobs:
gatekeeper:
runs-on: ubuntu-latest
steps:
- name: 检出代码
uses: actions/checkout@v4
- name: 准备 Python纯标准库无需装依赖
uses: actions/setup-python@v5
with:
python-version: "3.9"
# 安装 gitlink-cli示意按 runner 实际情况替换为正确的安装/分发方式)
- name: 安装 gitlink-cli
run: |
# 例如从发布物下载或用包管理器安装,确保 PATH 里有 gitlink-cli
gitlink-cli --version
# 跑门禁:采集本次 PR 上下文 → 评分 → 裁决。
# REQUEST_CHANGES 时脚本退出码为 2下面用 if/exit 把它转成 job 失败。
- name: 运行 PR 看门人门禁
env:
# gitlink-cli 通过该环境变量认证(对应仓库配置的 secret
GITLINK_TOKEN: ${{ secrets.GITLINK_TOKEN }}
# Gitea 注入的 PR 编号;不同 runner 字段名可能不同,按实际调整
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
set -o pipefail
python3 examples/workflows/pr-quality-gatekeeper/scripts/gatekeeper_workflow.py \
--owner "${{ github.repository_owner }}" \
--repo "${{ github.event.repository.name }}" \
--pr "${PR_NUMBER}" \
--policy skills/gitlink-gatekeeper/examples/gatekeeper.yaml \
--owner-rules workflow/owner-rules.yaml \
--cli-bin gitlink-cli
code=$?
# 退出码0 = PASS/COMMENT放行2 = REQUEST_CHANGES挡住让 job 失败);
# 1 = 可预期错误(缺参数 / 未装 gitlink-cli 等),同样视为失败。
if [ "$code" -eq 0 ]; then
echo "门禁通过PASS/COMMENT"
exit 0
elif [ "$code" -eq 2 ]; then
echo "::error::门禁裁决 REQUEST_CHANGES阻止合并"
exit 1
else
echo "::error::门禁执行出错(退出码 $code"
exit 1
fi
# 上传评分卡 / summary 产物,便于在 CI 页面查看裁决依据
- name: 上传门禁产物
if: always()
uses: actions/upload-artifact@v4
with:
name: gatekeeper-scorecard
path: outputs/

View File

@ -0,0 +1,19 @@
# config.example.yaml — PR 看门人闭环的工作流配置gatekeeper_workflow.py --config 用)
#
# 命令行参数(--owner/--repo/--pr/--policy/--owner-rules/--findings会覆盖这里的同名字段。
# 相对路径以本配置文件所在目录为基准解析。
owner: Gitlink # 目标仓库 ownerGitLink 主分支为 master
repo: gitlink-cli # 目标仓库名
pr: 1 # 目标 PR 编号(用 --pr 覆盖以复用同一配置跑不同 PR
# 策略文件Policy-as-Code。缺省 / 文件不存在时回退脚本内置默认策略SSOT 第 2 节)。
policy: ../skills/gitlink-gatekeeper/examples/gatekeeper.yaml
# 文件路径 → reviewer 路由表(工作流步骤 1
owner_rules: owner-rules.yaml
# 可选AI 审查发现 JSON注入 review_findings 维度)。
# 缺省为空 → 评分仍确定性可复现(同策略 + 同 PR 上下文 → 同裁决)。
# 这里默认指向随附的示例1 major + 2 minor开箱即可跑通用 --findings 覆盖为你自己的产物。
findings: findings.sample.json

View File

@ -0,0 +1,66 @@
# 架构与数据流 — PR 看门人闭环
本工作流采用「**采集 → 路由 → 裁决 → 回写/善后**」四段式流水线,把 `gitlink-gatekeeper` 的 Policy-as-Code 门禁包成一条可复现闭环。所有数值/字段/算法以已收录的 [`gitlink-gatekeeper` Skill REFERENCE](../../../../skills/gitlink-gatekeeper/REFERENCE.md) 为准。
## 设计目标
- **可复现**:同策略 + 同 PR 上下文 → 同评分卡 + 同裁决确定性算法SSOT 第 35 节)。
- **可审计**:评分卡逐维列分 + 备注,硬门禁逐条列出命中原因,裁决可追溯到具体规则与 `gatekeeper.yaml` 版本。
- **安全默认**:默认 dry-run写操作需显式 `--apply`绝不默认自动合并SSOT 第 8 节)。
- **低门槛**:仅依赖 `gitlink-cli` 与 Python 标准库(含内置 YAML 子集解析器,无第三方包)。
- **边界清晰**:采集、路由、裁决、回写四段各自独立,便于单测与替换(如换一套评分维度只动裁决段)。
## ASCII 流程图
> 下面是数据流占位图:左侧为 `gitlink-cli` 采集,中间为本脚本的确定性处理,右侧为回写/善后的写操作(仅 `--apply` 时执行)。
```
┌──────────────────────────── gatekeeper_workflow.py ────────────────────────────┐
│ │
gitlink-cli (读) │ step 1: 路由 step 2: 裁决 step 3: 回写 + 善后 │ gitlink-cli (写, 仅 --apply)
───────────────────── │ ─────────────────── ────────────────── ───────────────────────── │ ─────────────────────────────
│ │
pr +view ──┐ │ ┌─ review_findings(40) ─┐ │
pr +files ──┼──▶ 采集 │ changed_files │ test_coverage (20) │ verdict │ pr +comment ─▶ 评分卡评论
ci +builds │ 归一化 │ │ │ pr_hygiene (15) │──▶ ── PASS ───────┐ │
api .../ │ │ ▼ │ commit_quality(15) │ ── COMMENT ───┐ │ │ label +create ─▶ 裁决标签
commits ──┘ │ owner-rules.yaml │ ci_status (10) │ ── REQ_CHG ─┐ │ │ │ (+ 挂 issue_tag_ids
▲ │ (glob → reviewer) └───────────┬───────────┘ │ │ │ │ via Raw API POST
│ │ │ ▼ │ │ │ │ /:owner/:repo/issues/:id)
gatekeeper.yaml ─────┼─────┼──────────────▶ hard_gates 判定 ─┴─▶ total 0..100 ─▶ 阈值 │ │ │ │
(Policy-as-Code) │ ▼ (SSOT 第4节) (SSOT 第3节) (第5节) │ │ │ │ issue +create ─▶ tracking issue
│ │ suggested_reviewers ──────────────────────────────────────────┘ │ │ │ (仅 REQUEST_CHANGES)
findings.json ───────┼──▶ review_findings 注入 │ │ │
(可选, AI 审查) │ │ │ │ pr +merge ─▶ 合并 (受限:
│ ┌── outputs/*_scorecard.md ◀────────────┘ │ │ PASS + auto_merge + --apply)
│ 本地产物落盘 (总是) ───────┤ │ │
│ └── outputs/*_summary.json ◀──────────────┘ │
└─────────────────────────────────────────────────────────────────────────────────┘
dry-run默认右侧写操作仅打印「将要执行的命令」不实际调用 → 安全。
--apply :右侧写操作真正执行;其中合并需同时满足 PASS + 策略 auto_merge=true + --apply。
```
## 四段职责
### ① 采集collect_pr_context
调只读 `gitlink-cli` 命令拿到 PR 元信息、变更文件、CI 状态、commits端点未开放时降级不阻断。输出统一归一化为内部结构兼容 GitLink Envelope 的多种字段名。
### ② 路由route_reviewers
`owner-rules.yaml`,对每个变更文件按 glob 顺序匹配(首个命中生效,顺序即优先级),产出 `reviewer → 文件清单`;未命中文件归 `default_reviewers`。结果写进评分卡的「Suggested reviewers」分区。**只产出建议,不调用任何写操作**——是否真正分配由维护者决定。
### ③ 裁决score_dimensions / evaluate_hard_gates / decide_verdict
- 五维加权评分(权重和=100SSOT 第 3 节),可选注入 AI findings 影响 `review_findings`
- 硬门禁逐项判定SSOT 第 4 节),任一命中即 `hard_gate_failed`
- 裁决判定树SSOT 第 5 节):硬门禁失败 → REQUEST_CHANGES否则按总分与 `pass`/`request_changes` 阈值落三态。
- 渲染评分卡SSOT 第 6 节模板)。
### ④ 回写 + 善后build_*_command + execute_write
按裁决构造写操作计划评分卡评论、裁决标签、REQUEST_CHANGES 时tracking issue、受限合并。dry-run 只打印计划;`--apply` 才逐条执行并记录结果到 `summary.json`
## 为什么选这条链路
子赛题三要求用现有命令 / Skill 组合形成完整解决方案,且串联不少于 3 步。本链路:
1. 串联了 **4 个只读采集命令** + **最多 4 个写命令**,远超 3 步下限。
2. 形成从「数据获取」到「治理动作落地」的端到端闭环,并能接入 CIREQUEST_CHANGES 返回码 2
3. 复用本作品自研的 `label` 命令组(子赛题一)与 gatekeeper 策略(子赛题二),三个子赛题在同一作品内闭环,相互增强。

View File

@ -0,0 +1,48 @@
# 最短复现路径3 步)
## 1. 安装与认证
```bash
npm install -g @gitlink-ai/cli # ≥0.2.0label 命令与 gitlink-gatekeeper Skill 已内置)
gitlink-cli auth login # 或 export GITLINK_TOKEN=<私人令牌>
gitlink-cli auth status # 确认已登录
```
## 2. 对任意真实 PR 出评分卡dry-run零写入
在本目录(`examples/workflows/pr-quality-gatekeeper/`)下:
```bash
python3 scripts/gatekeeper_workflow.py \
--owner Gitlink --repo gitlink-cli --pr <PR号> \
--policy ../../../skills/gitlink-gatekeeper/examples/gatekeeper.yaml \
--owner-rules owner-rules.example.yaml \
--output-dir outputs
```
产物:`outputs/<owner>_<repo>_pr<id>_scorecard.md`(评分卡)+ `_summary.json`(结构化摘要)。
退出码:`0` = PASS/COMMENT`2` = REQUEST_CHANGES可直接当 CI 门禁用),`1` = 运行错误。
不带 `--policy` 也能跑(脚本内置同值默认策略);想看含扣分的评分卡,加 `--findings findings.example.json`
## 3. 可选进阶
- **真实回写**(评论 + 裁决标签 + tracking issue`--apply`。请先在自有 fork 演练;自动裁决只用建议性 `common` 评论,绝不替人 approve/reject绝不自动合并。
- **全仓体检**(只读批扫全部 open PR出治理报告
```bash
python3 scripts/gatekeeper_sweep.py \
--owner Gitlink --repo gitlink-cli \
--policy ../../../skills/gitlink-gatekeeper/examples/gatekeeper.yaml \
--owner-rules owner-rules.example.yaml \
--output-dir sweep-out --date-label $(date +%F)
```
- **CI 接入**:见 [`../ci-example/`](../ci-example/)Gitea ActionsPR 触发自动门禁)。
- **改门禁松紧**:复制一份 `gatekeeper.yaml``weights/hard_gates/thresholds`,字段说明见 [Skill REFERENCE](../../../../skills/gitlink-gatekeeper/REFERENCE.md)。
## 验证自己改动没破坏确定性
```bash
python3 tests/test_scoring.py # 同输入 → 同分 → 同裁决 的回归护栏
```

View File

@ -0,0 +1,106 @@
# 运行手册 — PR 看门人闭环
本手册覆盖 `scripts/gatekeeper_workflow.py` 的前置条件、运行步骤、参数、预期输出与回滚。数值/字段以已收录的 [`gitlink-gatekeeper` Skill REFERENCE](../../../../skills/gitlink-gatekeeper/REFERENCE.md) 为准。
## 1. 前置条件
- 已安装 `gitlink-cli` 且在 `PATH` 中(或用 `--cli-bin` 指定路径)。
- 已完成登录:`gitlink-cli auth login`Token 有效期 7 天,过期重新登录;详见 [gitlink-shared](../../../../skills/gitlink-shared/SKILL.md))。
- 对目标仓库有读权限;要 `--apply` 回写评论/标签/建 issue 时需写权限。
- Python 3.8+(脚本纯标准库,无需 `pip install`)。
验证登录态:
```bash
gitlink-cli auth status
gitlink-cli pr +view -i <pr_id> --owner <owner> --repo <repo> --format json # 确认目标 PR 可读
```
## 2. 配置
编辑 [`../config.example.yaml`](../config.example.yaml)(或复制一份),填好 `owner`/`repo`/`pr` 与策略、路由表路径。命令行参数会覆盖配置同名字段,相对路径以配置文件所在目录为基准。
按需调整 [`../owner-rules.example.yaml`](../owner-rules.example.yaml):把占位 reviewer 用户名替换成本仓库维护者,按「具体规则在前」排列 glob。
按需选择策略预设(均在 [`../../../../skills/gitlink-gatekeeper/examples/`](../../../../skills/gitlink-gatekeeper/examples/)
- `gatekeeper.yaml`:均衡基线(= SSOT 内置默认)。
- `gatekeeper.strict.yaml`:严格预设。
- `gatekeeper.lenient.yaml`:宽松预设。
- 不指定 `--policy` 且配置无 `policy` 字段时,回退脚本内置默认策略(与 `gatekeeper.yaml` 等价)。
## 3. 运行步骤
### 步骤 Adry-run 预览(安全默认,必做)
```bash
python3 scripts/gatekeeper_workflow.py --config config.example.yaml --pr <pr_id>
```
此模式**不写任何东西**,只采集 + 评分 + 打印将要执行的写命令 + 落盘本地产物。先看评分卡与计划是否符合预期。
### 步骤 B注入 AI 审查发现(可选)
`review_findings` 维度默认按 0 发现计分。若已有 AI 代码审查(如 `gitlink-code-review` Skill产出整理成 JSON 注入:
```json
{ "findings": [
{ "severity": "blocker", "message": "硬编码密钥", "file": "internal/auth/refresh.go", "line": 12 },
{ "severity": "minor", "message": "缺超时上下文", "file": "internal/auth/handler.go", "line": 40 }
] }
```
```bash
python3 scripts/gatekeeper_workflow.py --config config.example.yaml --pr <pr_id> --findings findings.json
```
`severity` 取值:`blocker` / `major` / `minor` / `nit`(其余忽略)。
### 步骤 Capply 执行写操作
确认 dry-run 计划无误后,加 `--apply`
```bash
python3 scripts/gatekeeper_workflow.py --config config.example.yaml --pr <pr_id> --apply
```
将依次执行(按裁决):回写评分卡评论 → 确保裁决标签存在 →(仅 REQUEST_CHANGES创建 tracking issue。
**合并不会自动发生**:仅当策略 `behavior.auto_merge: true` 且裁决为 `PASS` 且本次带 `--apply` 时,才追加 `pr +merge`。默认 `auto_merge: false`
## 4. 参数速查
| 参数 | 说明 | 默认 |
|------|------|------|
| `--config` | 工作流配置 YAMLowner/repo/pr/policy/owner_rules/findings | 无 |
| `--owner` / `--repo` / `--pr` | 覆盖配置中的目标 | 取自 config |
| `--policy` | `gatekeeper.yaml` 路径 | 内置默认策略 |
| `--owner-rules` | `owner-rules.yaml` 路径 | 取自 config |
| `--findings` | AI 审查发现 JSON | 空0 发现) |
| `--cli-bin` | `gitlink-cli` 可执行路径 | `gitlink-cli` |
| `--skip-ci` | 跳过 CI 采集(`ci_status` 记 `unknown` | 否 |
| `--output-dir` | 本地产物目录 | `outputs` |
| `--apply` | **执行写操作**;不传则仅预览 | 否dry-run |
## 5. 预期输出
- 终端:三段进度(路由 / 裁决 / 回写)+ 评分概览 + 计划或执行结果 + 最终裁决。
- 文件:
- `outputs/<owner>_<repo>_pr<id>_scorecard.md` — 评分卡SSOT 第 6 节模板)。
- `outputs/<owner>_<repo>_pr<id>_summary.json` — 结构化摘要(路由、各维得分、硬门禁、裁决、`planned_writes`、`executed`、产物路径)。
- 退出码:`PASS`/`COMMENT` → `0``REQUEST_CHANGES` → `2`(可作 CI 门禁);可预期错误(缺配置 / 未登录 / CLI 缺失)→ `1`
样例评分卡见 [`../../../../skills/gitlink-gatekeeper/examples/scorecard-sample.md`](../../../../skills/gitlink-gatekeeper/examples/scorecard-sample.md)。
## 6. 回滚
dry-run 不产生任何远端副作用,无需回滚(本地产物可直接删 `outputs/`)。
`--apply` 后如需撤销:
| 已做的写操作 | 回滚方式 |
|--------------|----------|
| 回写的评分卡评论 | 评论走 issue journals在 PR 页面手动删除该评论即可;脚本不提供删除命令(避免误删他人评论) |
| 创建的裁决标签定义 | `gitlink-cli label +delete -i <label_id> --owner <o> --repo <r>`(先 `label +list` 查 id |
| 创建的 tracking issue | `gitlink-cli issue +close -n <number> --owner <o> --repo <r>`(关闭而非删除,保留审计痕迹) |
| 已合并的 PR | **不可自动回滚**。这也是默认 `auto_merge: false` 的原因;合并前务必人工确认。如确需撤销,按仓库常规流程 revert commit |
> 安全提示:任何 `--apply` 写操作前,脚本会在 dry-run 计划里完整复述将执行的命令。生产仓库建议先 dry-run`--apply`

View File

@ -0,0 +1,55 @@
# 真实平台验证记录
> 全部针对 **GitLink 线上真实平台** 运行(`gitlink-cli` + Token 认证),非 mock。
> 他人仓库一律 dry-run只读零写入写操作只在自有 fork 演练。
> 运行环境macOSApple Silicon· Python 3.9 · `@gitlink-ai/cli` 0.2.0npm 官方发布版,零本地构建)。
## A. dry-run真实 PR → PASS 90/100
对本仓库真实 PR`pull_request_id 15222`feat/org-team-projects8 个变更文件):
- 路由正确分流README/docs/skill → doc-maintainer`org.go` → go-reviewer`org_test.go` → qa-reviewer
- 评分确定性可手算复现review 40/40 · test 20/201 src/1 test· hygiene 10/15desc✓/issue✗/size✓· commit 15/15 · ci 5/10unknown= **90 → PASS**
- CI 取不到构建记录 → `unknown`:按策略**不触发**硬门禁(仅显式 `failing` 触发),只在 CI 维记半分
- 产物:[`../examples/demo-outputs/scorecard-pass-90.md`](../examples/demo-outputs/scorecard-pass-90.md)
## B. 注入真实审查发现 → REQUEST_CHANGES 55/100
同一 PR注入 [`../findings.example.json`](../findings.example.json) 重跑review_findings 40/40 → 5/401 major + 2 minor总分 90 → 55**裁决翻转为 REQUEST_CHANGES**。
**发现是真的,不是编的**——三条均来自对该 PR 真实 diffhead `bcc27bf`)的代码审查,标注 `shortcuts/org/org.go` 真实行号,任何人拉取该分支可逐条复核。其中 major新增的 `parseBool` 只认字面 `"true"``--dry-run=1` 会被静默当 false而该 flag 守护的是「批量移除团队全部项目」这一破坏性操作。
产物:[`../examples/demo-outputs/scorecard-findings-55.md`](../examples/demo-outputs/scorecard-findings-55.md)
## C. `--apply` 真实回写(自有 fork 演练)
在自有 fork 的演练 PR故意「改源码不带测试」上执行 `--apply`
- 触发硬门禁 `require_tests_for_src_changes` → REQUEST_CHANGES 40/100
- GitLink API 回执确认三件写操作全部落地:
1. 评分卡评论回写到 PRcomment id `472741`
2. 自动创建 tracking issueid `143217`),汇总硬门禁 + 必修项 + 建议 reviewer与 PR 双向回链
3. 裁决标签挂载到 PR 背后 issue`label +list` 查 id → Raw API `POST /:owner/:repo/issues/<issue_id>`)——依赖本仓库的 `label` 命令0.2.0 起官方发布版自带)
## D. 全仓批量体检113 个 open PR
`gatekeeper_sweep.py` 对本仓库**全部 113 个 open PR** 逐个 dry-run只读、零写入、零 AI 成本113/113 成功:
- 裁决分布:**PASS 105 · COMMENT 6 · REQUEST_CHANGES 2**;分数 min 70 / 中位 90 / 均值 88.5 / max 95
- 治理洞察:**96% 的 open PR 未关联 issue**2 个 PR 触发 `require_tests_for_src_changes`(改源码不带测试)
- 完整报告(含全量明细表):[`../examples/demo-outputs/sweep-report-2026-06-10.md`](../examples/demo-outputs/sweep-report-2026-06-10.md)
- 诚实口径批扫不注入审查发现review_findings 维未评、按满分计CI 统一 `--skip-ci`unknown 半分)——总分代表「除人工/AI 审查外的工程卫生分」,偏乐观
## E. 单元测试(确定性回归护栏)
```bash
$ python3 tests/test_scoring.py
OK
```
锁定四个权威裁决案例PASS / REQUEST_CHANGES / COMMENT / 硬门禁直拒)的**总分与裁决**与 Skill 文档逐位一致;任何改动若破坏「同输入 → 同分 → 同裁决」,测试立即变红。
## 真实运行当场暴露过的问题(透明记录)
- GitLink 的 PR 标题/描述在 `pr +view` 返回的 `issue.subject/description`,而非 `pull_request` 子对象——离线 mock 测不到,真实平台运行才暴露并修复。
- npm 0.1.18 时代 `--apply` 的打标签步骤会报 `unknown command "label"`(彼时 `label` 命令尚未发布0.2.0 起官方发布版自带,整条闭环零本地构建跑通。

View File

@ -0,0 +1,28 @@
## 🛡️ Gatekeeper Report — PR #15222 feat(org): add team project binding shortcuts
**Verdict: ❌ REQUEST_CHANGES** · Score: 55/100 · policy: gatekeeper.yaml@v1
| Dimension | Weight | Score | Notes |
|-----------|:------:|:-----:|-------|
| Review findings | 40 | 5/40 | 0 blocker / 1 major / 2 minor / 0 nit |
| Test coverage | 20 | 20/20 | 1 src / 1 test files |
| PR hygiene | 15 | 10/15 | desc ✓ / linked issue ✗ / size ✓ |
| Commit quality | 15 | 15/15 | 0/0 conventional |
| CI status | 10 | 5/10 | unknown |
### 👥 Suggested reviewers (3)
- @doc-maintainer — 6 file(s): README.md, README.zh-CN.md, doc/changes/org-team-projects.md …
- @go-reviewer — 2 file(s): shortcuts/org/org.go, shortcuts/org/org_test.go
- @qa-reviewer — 1 file(s): shortcuts/org/org_test.go
### 🔴 Must fix (1)
- [major] parseBool 只把字面 "true" 当真:用户传 --dry-run=1 / t / TRUE 以外写法会被静默解析为 false。该 flag 守护的是 team-projects-remove-all批量移除团队全部项目这类破坏性操作——预览意图被静默降级为真实执行。建议改用 strconv.ParseBool与 shortcuts/common/runner.go:52 解析 flag 默认值的行为一致),无法识别的值应报错而非吞掉 — shortcuts/org/org.go:156
### 🟡 Should fix (2)
- [minor] team-projects-remove-all 一条命令清空团队全部项目绑定,除 --dry-run 外无确认机制;建议在 Description/help 标注危险性,或要求显式 --yes 二次确认 — shortcuts/org/org.go:103
- [minor] dry-run 预览 payload 中 actionadd_all_team_projects/remove_all_team_projects与实际请求 path 段create_all/destroy_all见 :134是两套词汇排查问题时易误导建议统一或在 payload 同时给出两者映射 — shortcuts/org/org.go:142
### Next steps
1. 评分低于阈值,按上方 Must/Should fix 修复后重新触发 gatekeeper
---
*Generated by gitlink-gatekeeper · policy-as-code PR gate · re-run after changes*

View File

@ -0,0 +1,21 @@
## 🛡️ Gatekeeper Report — PR #15222 feat(org): add team project binding shortcuts
**Verdict: ✅ PASS** · Score: 90/100 · policy: gatekeeper.yaml@v1
| Dimension | Weight | Score | Notes |
|-----------|:------:|:-----:|-------|
| Review findings | 40 | 40/40 | 0 blocker / 0 major / 0 minor / 0 nit |
| Test coverage | 20 | 20/20 | 1 src / 1 test files |
| PR hygiene | 15 | 10/15 | desc ✓ / linked issue ✗ / size ✓ |
| Commit quality | 15 | 15/15 | 0/0 conventional |
| CI status | 10 | 5/10 | unknown |
### 👥 Suggested reviewers (3)
- @doc-maintainer — 6 file(s): README.md, README.zh-CN.md, doc/changes/org-team-projects.md …
- @go-reviewer — 2 file(s): shortcuts/org/org.go, shortcuts/org/org_test.go
- @qa-reviewer — 1 file(s): shortcuts/org/org_test.go
### Next steps
1. 满足合并门禁;如策略开启 auto_merge 且操作者带 --apply可执行合并
---
*Generated by gitlink-gatekeeper · policy-as-code PR gate · re-run after changes*

View File

@ -0,0 +1,138 @@
# gatekeeper 仓库体检报告 —— Gitlink/gitlink-cli2026-06-10
> 对 **113 个 open PR** 全量 dry-run**只读,零写入**)· 策略 `gatekeeper.yaml` · 成功 113 / 失败 0
>
> **诚实口径**:批扫未注入 AI 审查发现review_findings 维按 0 发现计满分(**该维度未评**CI 维按 `--skip-ci` 统一记 unknown半分。其余维度为真实采集。因此**总分代表「除人工/AI 审查外的工程卫生分」,偏乐观**;裁决分布同理。
## 总览
- 裁决分布COMMENT **6** · PASS **105** · REQUEST_CHANGES **2**
- 分数min 70 / 中位 90 / 均值 88.5 / max 95
- **0%** 的 PR 测试覆盖维 0 分(改动不带任何测试)
- **96%** 的 PR 未关联 issue
- **2%** 的 PR 触发 REQUEST_CHANGES硬门禁或低分
硬门禁命中:`require_tests_for_src_changes` × 2
## 全量明细(按分数降序)
| PR | 标题 | 作者 | 总分 | 裁决 | 硬门禁失败 | 卫生(描述/关联/体量) |
|----|------|------|-----:|------|-----------|---------------------|
| [#145](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/145) | fix(issue): preserve metadata during batch close | dtwdtw | 95 | PASS | — | ✓/✓/✓ |
| [#218](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/218) | feat(skills): 新增 科研Fork影响力分析 的skill : gitlink-re | yangsai | 90 | PASS | — | ✓/✗/✓ |
| [#177](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/177) | feat(wiki): add wiki management shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
| [#217](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/217) | feat(commands): add command catalog export | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
| [#216](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/216) | feat(api): support saved variables in batch plan | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
| [#214](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/214) | feat(pr): add conversation comment shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
| [#213](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/213) | feat(repo): add mirror sync shortcut | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
| [#212](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/212) | feat(feedback): add feedback shortcut | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
| [#211](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/211) | feat(repo): add profile view shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
| [#210](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/210) | feat(skills): 新增维护者交接与分支治理 Skills | Mengz | 90 | PASS | — | ✓/✗/✓ |
| [#208](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/208) | feat(user): add pinned project shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
| [#207](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/207) | feat(user): add statistics shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
| [#206](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/206) | feat(commit): add commit inspection shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
| [#204](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/204) | feat(org): 增强组织团队与成员管理快捷命令 | Mengz | 90 | PASS | — | ✓/✗/✓ |
| [#203](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/203) | feat(user): 增加用户画像分析快捷命令 | Mengz | 90 | PASS | — | ✓/✗/✓ |
| [#202](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/202) | feat(ignore): add ignore template shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
| [#201](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/201) | feat(account): add account auth shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
| [#200](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/200) | feat(pr): add review journal shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
| [#199](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/199) | feat(code): add read-only code browsing shortcut | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
| [#198](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/198) | feat(message): 增加消息中心快捷命令 | Mengz | 90 | PASS | — | ✓/✗/✓ |
| [#197](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/197) | feat(message-settings): 增加消息通知设置快捷命令 | Mengz | 90 | PASS | — | ✓/✗/✓ |
| [#194](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/194) | fix(pr): 补齐 pr +view 的合并与关闭时间字段 | Mengz | 90 | PASS | — | ✓/✗/✓ |
| [#193](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/193) | feat(shortcut): add shortcuts/wiki | co63oc | 90 | PASS | — | ✓/✗/✓ |
| [#192](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/192) | feat(repo): add navigation unit shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
| [#191](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/191) | feat(user): add profile shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
| [#187](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/187) | feat(org): add team project bulk shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
| [#186](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/186) | feat(ref): add branch and tag shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
| [#185](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/185) | Add workflow pull request review queue | Mengz | 90 | PASS | — | ✓/✗/✓ |
| [#184](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/184) | Add workflow release notes generator | Mengz | 90 | PASS | — | ✓/✗/✓ |
| [#183](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/183) | feat(project): add lifecycle flow shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
| [#182](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/182) | feat(issue): add journal maintenance shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
| [#181](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/181) | feat(topic): add project topic shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
| [#180](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/180) | feat(template): add project template shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
| [#179](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/179) | feat(dataset): add research dataset shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
| [#178](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/178) | feat(contents): add repository content shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
| [#176](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/176) | feat(user): add dashboard shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
| [#175](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/175) | feat(notification): add message and setting shor | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
| [#174](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/174) | feat(public-key): add SSH key shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
| [#173](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/173) | feat(account): add cancellation shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
| [#172](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/172) | feat(account): add security shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
| [#171](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/171) | feat(oauth): add token shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
| [#170](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/170) | Add repository file search and batch commit shor | Mengz | 90 | PASS | — | ✓/✗/✓ |
| [#167](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/167) | feat(account): add email verification shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
| [#164](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/164) | Add pull request review comment management short | Mengz | 90 | PASS | — | ✓/✗/✓ |
| [#163](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/163) | Add complete issue comment management shortcuts | Mengz | 90 | PASS | — | ✓/✗/✓ |
| [#160](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/160) | Add GitLink feedback submission shortcut | Mengz | 90 | PASS | — | ✓/✗/✓ |
| [#158](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/158) | Add code trace analysis shortcuts | Mengz | 90 | PASS | — | ✓/✗/✓ |
| [#153](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/153) | feat(shortcut): add shortcuts/ignore | co63oc | 90 | PASS | — | ✓/✗/✓ |
| [#151](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/151) | feat(transfer): add transfer request shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
| [#135](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/135) | feat(dev): add developer resource shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
| [#118](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/118) | feat(access): add project access shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
| [#114](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/114) | feat(mirror): add mirror repository shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
| [#113](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/113) | feat(todo): add request approval shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
| [#107](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/107) | feat(star): add starred project shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
| [#83](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/83) | feat(org): add team project binding shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
| [#82](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/82) | feat(meta): add attachment and metadata shortcut | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
| [#78](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/78) | feat(branch): complete OpenAPI shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
| [#76](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/76) | feat(notification): add OpenAPI shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
| [#72](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/72) | feat(template): add project template shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
| [#70](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/70) | feat(user): add account and stats shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
| [#65](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/65) | feat(wiki): add OpenAPI shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
| [#64](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/64) | feat(dataset): add OpenAPI shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
| [#63](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/63) | feat(code): add repository code OpenAPI shortcut | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
| [#152](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/152) | chore(doc): fix README.md | co63oc | 90 | PASS | — | ✓/✗/✓ |
| [#137](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/137) | feat(skills): 增强 7 个 Agent Skill + 新增 2 个 Skill | whale | 90 | PASS | — | ✓/✗/✓ |
| [#149](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/149) | feat(skills): 新增 学者/团队科研画像生成 的skill : gitlink-sc | yangsai | 90 | PASS | — | ✓/✗/✓ |
| [#148](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/148) | feat(skills): 新增 科研热点追踪与知识图谱构建 的skill : gitlink- | yangsai | 90 | PASS | — | ✓/✗/✓ |
| [#144](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/144) | feat(skills): 新增 3 个 Agent Skill — wiki-builder, | whale | 90 | PASS | — | ✓/✗/✓ |
| [#134](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/134) | 新增 shell 自动补全命令 | Mengz | 90 | PASS | — | ✓/✗/✓ |
| [#99](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/99) | 新增 5 个仓库检查快捷命令 (languages/contributors/files/tag | jiangtx | 90 | PASS | — | ✓/✗/✓ |
| [#86](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/86) | fix: preserve issue metadata on update | dtwdtw | 90 | PASS | — | ✓/✗/✓ |
| [#73](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/73) | feat(user): add SSH key shortcuts | Mengz | 90 | PASS | — | ✓/✗/✓ |
| [#67](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/67) | feat(repo): add repository units shortcuts | Mengz | 90 | PASS | — | ✓/✗/✓ |
| [#60](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/60) | feat: add notification shortcuts | Mengz | 90 | PASS | — | ✓/✗/✓ |
| [#58](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/58) | feat: add repository reaction shortcuts | Mengz | 90 | PASS | — | ✓/✗/✓ |
| [#126](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/126) | feat(skills): 新增 gitlink-scaffold 社区健康文件体检 Skill | Ct201314 | 90 | PASS | — | ✓/✗/✓ |
| [#56](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/56) | feat: add git tag shortcut group | Mengz | 90 | PASS | — | ✓/✗/✓ |
| [#125](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/125) | feat(skills): 新增 gitlink-newcomer 新人引导 Skill | Ct201314 | 90 | PASS | — | ✓/✗/✓ |
| [#127](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/127) | feat(skills): 新增 gitlink-deps 依赖追踪 Skill | Ct201314 | 90 | PASS | — | ✓/✗/✓ |
| [#128](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/128) | feat(skills): 新增 gitlink-contributor 贡献者致谢与成长 Sk | Ct201314 | 90 | PASS | — | ✓/✗/✓ |
| [#129](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/129) | feat(skills): 新增 gitlink-kb 知识库问答 Skill | Ct201314 | 90 | PASS | — | ✓/✗/✓ |
| [#115](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/115) | feat: add catalog template shortcuts | Mengz | 90 | PASS | — | ✓/✗/✓ |
| [#116](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/116) | 新增仓库洞察快捷命令 | Mengz | 90 | PASS | — | ✓/✗/✓ |
| [#119](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/119) | 新增仓库转移快捷命令 | Mengz | 90 | PASS | — | ✓/✗/✓ |
| [#122](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/122) | 完善仓库 README 快捷命令 | Mengz | 90 | PASS | — | ✓/✗/✓ |
| [#50](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/50) | feat: add wiki shortcut group | Mengz | 90 | PASS | — | ✓/✗/✓ |
| [#54](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/54) | gitlink-growth 开源贡献者成长系统 Skill 贡献 | yingjie | 90 | PASS | — | ✓/✗/✓ |
| [#23](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/23) | feat: support fork metadata in pr create | Mengz | 90 | PASS | — | ✓/✗/✓ |
| [#196](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/196) | feat(release): 增加发布资产管理快捷命令 | Mengz | 88 | PASS | — | ✓/✗/✓ |
| [#215](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/215) | fix(client): improve API robustness | wangyue111 | 87 | PASS | — | ✓/✗/✓ |
| [#147](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/147) | feat(shortcuts): 新增 wiki/commit/file/star/watch | chroe | 86 | PASS | — | ✓/✗/✓ |
| [#209](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/209) | feat(milestone): 增加里程碑进度分析快捷命令 | Mengz | 85 | PASS | — | ✓/✗/✓ |
| [#205](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/205) | fix(issue): 修复详情缺失并保护更新元数据 | Mengz | 85 | PASS | — | ✓/✗/✓ |
| [#195](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/195) | feat(compare): 新增 compare 汇总与提交筛选能力 | Mengz | 85 | PASS | — | ✓/✗/✓ |
| [#190](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/190) | Add workflow release readiness gate | Mengz | 85 | PASS | — | ✓/✗/✓ |
| [#189](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/189) | Add workflow duplicate issue detection | Mengz | 85 | PASS | — | ✓/✗/✓ |
| [#188](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/188) | Add workflow dependency risk audit | Mengz | 85 | PASS | — | ✓/✗/✓ |
| [#165](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/165) | feat(issue): add batch maintenance shortcuts | wangyue111 | 85 | PASS | — | ✓/✗/✓ |
| [#159](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/159) | Add member application workflow shortcuts | Mengz | 85 | PASS | — | ✓/✗/✓ |
| [#77](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/77) | feat(journal): add issue and PR comment shortcut | wangyue111 | 85 | PASS | — | ✓/✗/✓ |
| [#150](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/150) | 新增 Issue 批量导出命令 | Mengz | 85 | PASS | — | ✓/✗/✓ |
| [#142](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/142) | 新增 PR 本地检出命令 | Mengz | 85 | PASS | — | ✓/✗/✓ |
| [#100](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/100) | 查看指定时间范围的开发统计 | jiangtx | 85 | PASS | — | ✗/✗/✓ |
| [#101](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/101) | 查看用户项目动态 | jiangtx | 85 | PASS | — | ✗/✗/✓ |
| [#21](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/21) | feat: add attachment shortcut group | Mengz | 85 | PASS | — | ✓/✗/✓ |
| [#139](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/139) | feat(wiki): 新增 Wiki 页面与目录管理 Shortcuts | whale | 82 | COMMENT | — | ✓/✗/✓ |
| [#130](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/130) | feat(workflows): 新增 gitlink-flow 社区运营自动化端到端工作流 | Ct201314 | 82 | COMMENT | — | ✓/✗/✓ |
| [#97](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/97) | 基础设施修复 | jiangtx | 81 | COMMENT | — | ✗/✗/✓ |
| [#123](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/123) | 新增 Release 资产下载命令 | Mengz | 80 | COMMENT | — | ✗/✗/✓ |
| [#103](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/103) | feat: 新建 pm 模块,添加 6 条项目管理命令 | wyxttn | 78 | COMMENT | — | — |
| [#131](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/131) | 子赛题三 - Java-Gatekeeper 端到端自动化质量门禁工作流 | xxxx12 | 75 | REQUEST_CHANGES | require_tests_for_src_changes | ✓/✓/✓ |
| [#30](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/30) | 增加wiki管理的shortcut | camelliamc | 74 | COMMENT | — | — |
| [#146](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/146) | feat: 新增 Showcase Dashboard 交互式展示页 | chroe | 70 | REQUEST_CHANGES | require_tests_for_src_changes | ✓/✗/✓ |
## 这份报告说明了什么
- 同一份 `gatekeeper.yaml` 策略可以**无人值守地体检一个真实活跃仓库的全部积压**——确定性评分意味着大规模治理零 AI 成本AI 只在需要语义判断review_findings时按需介入。
- 任何人重跑本报告(`python3 scripts/gatekeeper_sweep.py`)会对同一组 PR 得到同样的分数与裁决。

View File

@ -0,0 +1,23 @@
{
"_comment": "对 Gitlink/gitlink-cli PR #15222feat/org-team-projectshead bcc27bf真实 diff 的人工+AI 代码审查发现。每条均可在该 PR 的 shortcuts/org/org.go 对应行号复核——非样例数据。",
"findings": [
{
"severity": "major",
"file": "shortcuts/org/org.go",
"line": 156,
"message": "parseBool 只把字面 \"true\" 当真:用户传 --dry-run=1 / t / TRUE 以外写法会被静默解析为 false。该 flag 守护的是 team-projects-remove-all批量移除团队全部项目这类破坏性操作——预览意图被静默降级为真实执行。建议改用 strconv.ParseBool与 shortcuts/common/runner.go:52 解析 flag 默认值的行为一致),无法识别的值应报错而非吞掉"
},
{
"severity": "minor",
"file": "shortcuts/org/org.go",
"line": 103,
"message": "team-projects-remove-all 一条命令清空团队全部项目绑定,除 --dry-run 外无确认机制;建议在 Description/help 标注危险性,或要求显式 --yes 二次确认"
},
{
"severity": "minor",
"file": "shortcuts/org/org.go",
"line": 142,
"message": "dry-run 预览 payload 中 actionadd_all_team_projects/remove_all_team_projects与实际请求 path 段create_all/destroy_all见 :134是两套词汇排查问题时易误导建议统一或在 payload 同时给出两者映射"
}
]
}

View File

@ -0,0 +1,62 @@
# owner-rules.yaml — 文件路径 → reviewer 路由表gitlink-gatekeeper 工作流步骤 1
#
# 作用PR 看门人闭环的第一步「路由」。脚本拉取 PR 变更文件后,按下面的
# glob 规则把每个文件映射到建议 reviewer写进评分卡的「Suggested
# reviewers」分区REQUEST_CHANGES 时也写进 tracking issue
#
# 语义(见 scripts/gatekeeper_workflow.py route_reviewers
# - rules 按顺序匹配,**首个命中的规则生效**(顺序即优先级,把更具体的放前面)。
# - glob 用 Python fnmatch 语法(* 不跨目录段时也会匹配 /,与 fnmatch 行为一致)。
# - 一个文件命中后不再继续匹配后续规则;多个 reviewer 写在同一规则的 reviewers 列表里。
# - 未命中任何规则的文件归到 default_reviewers兜底
#
# 排序示例(首个命中即生效、顺序即优先级,请按需排序):
# 下面把目录 glob `skills/**` 放在语言 glob `**/*.go` 之前,于是 `skills/foo.go`
# 会先命中 `skills/**` → 路由给 skill-owner而不会落到 go-reviewer。若你希望
# skills 下的 Go 文件仍由 go-reviewer 审,就把语言规则提到目录规则之前
# (或在目录规则里收窄 glob如 `skills/**/*.md`)。
#
# 注意:本表只产出「建议」,不调用任何写操作;真正分配 reviewer 由维护者在
# PR 页面决定。gatekeeper 不替人点提交SSOT 第 8 节安全规则)。
#
# reviewers 填 GitLink 用户名login。下方为占位示例真实使用时替换为本仓库的维护者。
rules:
# —— 文档:只改文档走文档维护者,避免占用代码 reviewer ——
- glob: "docs/**"
reviewers: ["doc-maintainer"]
- glob: "**/*.md"
reviewers: ["doc-maintainer"]
# —— 工作流 / 脚本本体 ——
- glob: "workflow/**"
reviewers: ["workflow-owner"]
- glob: "skills/**"
reviewers: ["skill-owner"]
# —— 按语言路由到对应方向的 reviewer ——
- glob: "**/*_test.go"
reviewers: ["go-reviewer", "qa-reviewer"]
- glob: "**/*.go"
reviewers: ["go-reviewer"]
- glob: "test_*.py"
reviewers: ["py-reviewer", "qa-reviewer"]
- glob: "**/*.py"
reviewers: ["py-reviewer"]
- glob: "**/*.ts"
reviewers: ["fe-reviewer"]
- glob: "**/*.js"
reviewers: ["fe-reviewer"]
# —— 高敏感区CI / 依赖 / 安全配置,强制资深 reviewer ——
- glob: ".gitea/**"
reviewers: ["ci-owner", "security-reviewer"]
- glob: "**/Dockerfile"
reviewers: ["ci-owner"]
- glob: "go.mod"
reviewers: ["security-reviewer"]
- glob: "go.sum"
reviewers: ["security-reviewer"]
# 未命中上面任何规则的文件,兜底分配给这些人
default_reviewers: ["maintainer"]

View File

@ -0,0 +1,227 @@
# SPDX-License-Identifier: MulanPSL-2.0
"""gatekeeper_sweep —— 对一个仓库的全部 open PR 批量跑门禁(只读 dry-run出治理报告。
把单 PR 策略 评分卡 裁决升级为仓库级体检
1. 翻页拉取 PR 列表筛出 open
2. 逐个调用 gatekeeper_workflow.py强制 dry-run绝不 --apply对远端零写入
3. 汇总每个 PR summary.json 聚合统计 + 全量明细表 sweep-report.md / sweep-summary.json
诚实口径批扫不注入 AI 审查发现--findingsreview_findings 维按 0 发现计满分
报告中明确标注该维度未评其余 4 测试/卫生/commit/CI为真实采集结果
纯标准库无第三方依赖
"""
from __future__ import annotations
import argparse
import json
import re
import subprocess
import sys
import time
import urllib.request
from pathlib import Path
from typing import Any
API_BASE = "https://www.gitlink.org.cn/api"
HYGIENE_RE = re.compile(r"desc (✓|✗) / linked issue (✓|✗) / size (✓|✗)")
def fetch_open_prs(owner: str, repo: str, limit_pages: int = 20) -> list[dict[str, Any]]:
"""翻页拉取 PR 列表并筛出 open列表接口的 status 参数不可靠,按字段过滤)。"""
items: list[dict[str, Any]] = []
page = 1
while page <= limit_pages:
url = f"{API_BASE}/{owner}/{repo}/pulls.json?page={page}&limit=50"
with urllib.request.urlopen(url, timeout=30) as resp:
data = json.loads(resp.read().decode("utf-8"))
batch = data.get("issues") or []
if not batch:
break
items.extend(batch)
if len(items) >= int(data.get("search_count") or 0):
break
page += 1
return [it for it in items if it.get("pull_request_staus") == "open"]
def run_one(
workflow_script: Path,
owner: str,
repo: str,
number: int,
policy: Path,
owner_rules: Path,
cli_bin: str,
out_dir: Path,
) -> dict[str, Any]:
"""对单个 PR 跑一次 dry-run 门禁,返回解析后的行记录(失败不抛,记 error"""
cmd = [
sys.executable,
str(workflow_script),
"--owner", owner,
"--repo", repo,
"--pr", str(number),
"--policy", str(policy),
"--owner-rules", str(owner_rules),
"--cli-bin", cli_bin,
"--skip-ci",
"--output-dir", str(out_dir),
]
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=180)
slug = f"{owner}_{repo}_pr{number}".replace("/", "_")
summary_path = out_dir / f"{slug}_summary.json"
if proc.returncode == 1 or not summary_path.exists():
return {"number": number, "error": (proc.stderr or proc.stdout)[-200:].strip()}
summary = json.loads(summary_path.read_text(encoding="utf-8"))
hygiene = ""
scorecard_path = out_dir / f"{slug}_scorecard.md"
if scorecard_path.exists():
m = HYGIENE_RE.search(scorecard_path.read_text(encoding="utf-8"))
if m:
hygiene = "/".join(m.groups()) # 例如 "✓/✗/✓":描述/关联issue/体量
return {
"number": number,
"verdict": summary.get("verdict"),
"total": summary.get("total"),
"scores": summary.get("scores", {}),
"hard_gate_failures": [f.get("gate") if isinstance(f, dict) else f
for f in summary.get("hard_gate_failures", [])],
"hygiene": hygiene,
"suggested_reviewers": summary.get("routing", {}).get("suggested_reviewers", []),
}
def aggregate(rows: list[dict[str, Any]]) -> dict[str, Any]:
ok = [r for r in rows if "error" not in r]
totals = sorted(r["total"] for r in ok)
verdicts: dict[str, int] = {}
gate_hits: dict[str, int] = {}
for r in ok:
verdicts[r["verdict"]] = verdicts.get(r["verdict"], 0) + 1
for g in r["hard_gate_failures"]:
gate_hits[str(g)] = gate_hits.get(str(g), 0) + 1
def pct(n: int) -> str:
return f"{100 * n / len(ok):.0f}%" if ok else "0%"
no_linked = sum(1 for r in ok if r["hygiene"] and r["hygiene"].split("/")[1] == "")
zero_cov = sum(1 for r in ok if r["scores"].get("test_coverage") == 0)
return {
"scanned": len(rows),
"succeeded": len(ok),
"failed": len(rows) - len(ok),
"verdicts": verdicts,
"score_min": totals[0] if totals else None,
"score_median": totals[len(totals) // 2] if totals else None,
"score_avg": round(sum(totals) / len(totals), 1) if totals else None,
"score_max": totals[-1] if totals else None,
"hard_gate_hits": gate_hits,
"pct_zero_test_coverage": pct(zero_cov),
"pct_no_linked_issue": pct(no_linked),
"pct_request_changes": pct(verdicts.get("REQUEST_CHANGES", 0)),
}
def render_report(
owner: str, repo: str, policy_label: str, date_label: str,
rows: list[dict[str, Any]], agg: dict[str, Any],
pr_meta: dict[int, dict[str, Any]],
) -> str:
ok = [r for r in rows if "error" not in r]
lines = [
f"# gatekeeper 仓库体检报告 —— {owner}/{repo}{date_label}",
"",
f"> 对 **{agg['scanned']} 个 open PR** 全量 dry-run**只读,零写入**)· 策略 `{policy_label}` · "
f"成功 {agg['succeeded']} / 失败 {agg['failed']}",
">",
"> **诚实口径**:批扫未注入 AI 审查发现review_findings 维按 0 发现计满分(**该维度未评**"
"CI 维按 `--skip-ci` 统一记 unknown半分。其余维度为真实采集。"
"因此**总分代表「除人工/AI 审查外的工程卫生分」,偏乐观**;裁决分布同理。",
"",
"## 总览",
"",
f"- 裁决分布:{' · '.join(f'{k} **{v}**' for k, v in sorted(agg['verdicts'].items()))}",
f"- 分数min {agg['score_min']} / 中位 {agg['score_median']} / 均值 {agg['score_avg']} / max {agg['score_max']}",
f"- **{agg['pct_zero_test_coverage']}** 的 PR 测试覆盖维 0 分(改动不带任何测试)",
f"- **{agg['pct_no_linked_issue']}** 的 PR 未关联 issue",
f"- **{agg['pct_request_changes']}** 的 PR 触发 REQUEST_CHANGES硬门禁或低分",
"",
"硬门禁命中:" + ("".join(f"`{g}` × {n}" for g, n in sorted(agg["hard_gate_hits"].items(), key=lambda x: -x[1])) or ""),
"",
"## 全量明细(按分数降序)",
"",
"| PR | 标题 | 作者 | 总分 | 裁决 | 硬门禁失败 | 卫生(描述/关联/体量) |",
"|----|------|------|-----:|------|-----------|---------------------|",
]
for r in sorted(ok, key=lambda x: -x["total"]):
meta = pr_meta.get(r["number"], {})
title = str(meta.get("name", ""))[:48].replace("|", "\\|")
gates = ", ".join(str(g) for g in r["hard_gate_failures"]) or ""
lines.append(
f"| [#{r['number']}](https://www.gitlink.org.cn/{owner}/{repo}/pulls/{r['number']}) "
f"| {title} | {meta.get('author_name', '?')} | {r['total']} | {r['verdict']} | {gates} | {r['hygiene'] or ''} |"
)
errs = [r for r in rows if "error" in r]
if errs:
lines += ["", "## 跑失败的 PR", ""]
lines += [f"- #{r['number']}`{r['error']}`" for r in errs]
lines += [
"",
"## 这份报告说明了什么",
"",
"- 同一份 `gatekeeper.yaml` 策略可以**无人值守地体检一个真实活跃仓库的全部积压**——"
"确定性评分意味着大规模治理零 AI 成本AI 只在需要语义判断review_findings时按需介入。",
"- 任何人重跑本报告(`python3 scripts/gatekeeper_sweep.py`)会对同一组 PR 得到同样的分数与裁决。",
"",
]
return "\n".join(lines)
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="对全部 open PR 批量 dry-run 出治理报告")
parser.add_argument("--owner", default="Gitlink")
parser.add_argument("--repo", default="gitlink-cli")
parser.add_argument("--policy", type=Path, required=True)
parser.add_argument("--owner-rules", dest="owner_rules", type=Path, required=True)
parser.add_argument("--cli-bin", default="gitlink-cli")
parser.add_argument("--output-dir", type=Path, default=Path("sweep-outputs"))
parser.add_argument("--date-label", default="sweep", help="报告日期标签(可复现:不取系统时间)")
parser.add_argument("--max", type=int, default=0, help="只跑前 N 个0=全量),用于试跑")
parser.add_argument("--sleep", type=float, default=0.2, help="相邻 PR 间隔秒数(对平台礼貌)")
args = parser.parse_args(argv)
workflow_script = Path(__file__).with_name("gatekeeper_workflow.py")
runs_dir = args.output_dir / "runs"
runs_dir.mkdir(parents=True, exist_ok=True)
prs = fetch_open_prs(args.owner, args.repo)
if args.max:
prs = prs[: args.max]
pr_meta = {int(p["pull_request_number"]): p for p in prs}
print(f"open PR 共 {len(prs)}开始批扫dry-run零写入", flush=True)
rows: list[dict[str, Any]] = []
for i, p in enumerate(prs, 1):
number = int(p["pull_request_number"])
row = run_one(workflow_script, args.owner, args.repo, number,
args.policy, args.owner_rules, args.cli_bin, runs_dir)
rows.append(row)
tag = row.get("verdict", "ERROR")
print(f"[{i}/{len(prs)}] PR #{number}{tag} {row.get('total', '')}", flush=True)
time.sleep(args.sleep)
agg = aggregate(rows)
policy_label = args.policy.name
report = render_report(args.owner, args.repo, policy_label, args.date_label, rows, agg, pr_meta)
(args.output_dir / "sweep-report.md").write_text(report, encoding="utf-8")
(args.output_dir / "sweep-summary.json").write_text(
json.dumps({"aggregate": agg, "rows": rows}, ensure_ascii=False, indent=2),
encoding="utf-8",
)
print(f"\n报告:{args.output_dir / 'sweep-report.md'}")
print(f"汇总:{args.output_dir / 'sweep-summary.json'}")
print(f"裁决分布:{agg['verdicts']} · 均分 {agg['score_avg']}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,248 @@
#!/usr/bin/env python3
"""可复现评分单测 —— 把「同输入 → 同分 → 同裁决」从口号变成可验证事实。
纯标准库 unittestPython 3.9 兼容直接 import `scripts/gatekeeper_workflow.py`
的确定性算法score_dimensions / evaluate_hard_gates / decide_verdict对四个
权威裁决案例skills/gitlink-gatekeeper/examples/decision-*.md
scorecard-sample.md构造等价的 ScoreInput断言**总分****三态裁决**与文档逐位一致
任意一处算法改动若改变了这四个案例的分值本测试立即变红即为确定性的回归护栏
运行
python3 workflow/tests/test_scoring.py
python3 -m unittest workflow.tests.test_scoring # 在仓库根目录
数值来源默认策略 gatekeeper.yaml与脚本内置 DEFAULT_POLICY 一致
权重 40/20/15/15/10severity_penalty blocker=100/major=25/minor=5/nit=1
thresholds pass=85 / request_changes=60max_changed_files=80
"""
from __future__ import annotations
import importlib.util
import sys
import unittest
from pathlib import Path
# --------------------------------------------------------------------------- #
# 以绝对路径加载被测脚本(它在 scripts/ 下、非包,按文件直接载入最稳)
#
# 注意:必须先把模块塞进 sys.modules 再 exec —— 被测脚本用了
# `from __future__ import annotations`Python 3.9 的 @dataclass 在解析字符串
# 注解时会回查 sys.modules[cls.__module__],未注册会取到 None 而报
# AttributeError'NoneType' object has no attribute '__dict__')。
# --------------------------------------------------------------------------- #
_SCRIPT = (
Path(__file__).resolve().parent.parent / "scripts" / "gatekeeper_workflow.py"
)
_spec = importlib.util.spec_from_file_location("gatekeeper_workflow", _SCRIPT)
assert _spec and _spec.loader, f"无法定位被测脚本:{_SCRIPT}"
gw = importlib.util.module_from_spec(_spec)
sys.modules["gatekeeper_workflow"] = gw
_spec.loader.exec_module(gw) # type: ignore[union-attr]
ScoreInput = gw.ScoreInput
Finding = gw.Finding
score_dimensions = gw.score_dimensions
evaluate_hard_gates = gw.evaluate_hard_gates
decide_verdict = gw.decide_verdict
# 默认策略(深拷贝一份,避免任何用例意外改到共享 dict
import json as _json # noqa: E402
DEFAULT_POLICY = _json.loads(_json.dumps(gw.DEFAULT_POLICY))
# --------------------------------------------------------------------------- #
# 构造辅助:把「严重度计数 / 文件数 / commit 计数」翻译成 ScoreInput 字段
# --------------------------------------------------------------------------- #
def _findings(blocker: int = 0, major: int = 0, minor: int = 0, nit: int = 0):
"""按严重度计数生成 Finding 列表message/file/line 对评分无影响,仅 severity 计 penalty"""
out = []
for sev, n in (("blocker", blocker), ("major", major), ("minor", minor), ("nit", nit)):
for i in range(n):
out.append(Finding(severity=sev, message=f"{sev} #{i}", file="f.go", line=i + 1))
return out
def _commits(conforming: int, total: int):
"""生成 total 条 commit message其中 conforming 条符合 Conventional Commits。"""
assert conforming <= total
msgs = [f"feat(mod{i}): conforming change {i}" for i in range(conforming)]
msgs += [f"wip update {i}" for i in range(total - conforming)] # 'wip ...' 不匹配规约
return msgs
def _files(n: int):
"""生成 n 个占位变更文件路径(仅用于 size 维度计 lensrc/test 计数由字段直接给定)。"""
return [f"path/file_{i}.go" for i in range(n)]
def _build(
*,
pr_id: str,
title: str,
desc_len: int,
linked_issue: bool,
n_files: int,
src: int,
tests: int,
commits: tuple, # (conforming, total)
ci: str,
findings_counts: dict,
) -> ScoreInput:
description = "x" * desc_len if desc_len else ""
return ScoreInput(
pr_id=pr_id,
title=title,
description=description,
changed_files=_files(n_files),
changed_src=src,
changed_tests=tests,
commits=_commits(*commits),
ci_status=ci,
linked_issue=linked_issue,
findings=_findings(**findings_counts),
)
def _run(inp: ScoreInput):
"""跑完整确定性链路,返回 (total, verdict)。"""
dims = score_dimensions(inp, DEFAULT_POLICY)
failures = evaluate_hard_gates(inp, DEFAULT_POLICY)
verdict = decide_verdict(dims["total"], bool(failures), DEFAULT_POLICY)
return dims, failures, verdict
# --------------------------------------------------------------------------- #
# 四个权威案例
# --------------------------------------------------------------------------- #
class TestAuthoritativeCases(unittest.TestCase):
"""对照 examples/ 下四个裁决记录,断言总分与裁决。"""
def test_decision_pass(self):
# decision-pass.md3 src / 2 test、desc 142(含#198)、4/4 commit、CI passing、
# 0/0/1/2 findings → 33+17+15+15+10 = 90 → PASS
inp = _build(
pr_id="214",
title="feat(search): validate pagination params",
desc_len=142,
linked_issue=True,
n_files=5,
src=3,
tests=2,
commits=(4, 4),
ci="passing",
findings_counts={"minor": 1, "nit": 2},
)
dims, failures, verdict = _run(inp)
self.assertEqual(dims["review_findings"]["score"], 33)
self.assertEqual(dims["test_coverage"]["score"], 17)
self.assertEqual(dims["pr_hygiene"]["score"], 15)
self.assertEqual(dims["commit_quality"]["score"], 15)
self.assertEqual(dims["ci_status"]["score"], 10)
self.assertEqual(failures, [])
self.assertEqual(dims["total"], 90)
self.assertEqual(verdict, "PASS")
def test_decision_request_changes(self):
# decision-request-changes.md4 src / 0 test触发硬门禁
# require_tests_for_src_changes、desc 88 无关联、2/3 commit、CI passing、
# 0/1/1/2 findings → 8+0+10+10+10 = 38 → REQUEST_CHANGES
inp = _build(
pr_id="305",
title="refactor(billing): rework settlement pipeline",
desc_len=88,
linked_issue=False,
n_files=4,
src=4,
tests=0,
commits=(2, 3),
ci="passing",
findings_counts={"major": 1, "minor": 1, "nit": 2},
)
dims, failures, verdict = _run(inp)
self.assertEqual(dims["review_findings"]["score"], 8)
self.assertEqual(dims["test_coverage"]["score"], 0)
self.assertEqual(dims["pr_hygiene"]["score"], 10)
self.assertEqual(dims["commit_quality"]["score"], 10)
self.assertEqual(dims["ci_status"]["score"], 10)
gate_names = {f["gate"] for f in failures}
self.assertIn("require_tests_for_src_changes", gate_names)
self.assertEqual(dims["total"], 38)
self.assertEqual(verdict, "REQUEST_CHANGES")
def test_decision_comment(self):
# decision-comment.md2 src / 1 test、desc 52 无关联、2/3 commit、CI passing、
# 0/0/3/2 findings → 23+15+10+10+10 = 68 ∈ [60,85) 且无硬门禁 → COMMENT
inp = _build(
pr_id="277",
title="feat(config): merge defaults on load",
desc_len=52,
linked_issue=False,
n_files=2,
src=2,
tests=1,
commits=(2, 3),
ci="passing",
findings_counts={"minor": 3, "nit": 2},
)
dims, failures, verdict = _run(inp)
self.assertEqual(dims["review_findings"]["score"], 23)
self.assertEqual(dims["test_coverage"]["score"], 15)
self.assertEqual(dims["pr_hygiene"]["score"], 10)
self.assertEqual(dims["commit_quality"]["score"], 10)
self.assertEqual(dims["ci_status"]["score"], 10)
self.assertEqual(failures, [])
self.assertEqual(dims["total"], 68)
self.assertEqual(verdict, "COMMENT")
def test_scorecard_sample(self):
# scorecard-sample.md4 src / 0 test触发硬门禁、desc 64 无关联、3/4 commit、
# CI passing、0/1/2/1 findings → 4+0+10+11+10 = 35 → REQUEST_CHANGES
inp = _build(
pr_id="128",
title="feat(auth): add refresh-token rotation",
desc_len=64,
linked_issue=False,
n_files=6,
src=4,
tests=0,
commits=(3, 4),
ci="passing",
findings_counts={"major": 1, "minor": 2, "nit": 1},
)
dims, failures, verdict = _run(inp)
self.assertEqual(dims["review_findings"]["score"], 4)
self.assertEqual(dims["test_coverage"]["score"], 0)
self.assertEqual(dims["pr_hygiene"]["score"], 10)
self.assertEqual(dims["commit_quality"]["score"], 11)
self.assertEqual(dims["ci_status"]["score"], 10)
gate_names = {f["gate"] for f in failures}
self.assertIn("require_tests_for_src_changes", gate_names)
self.assertEqual(dims["total"], 35)
self.assertEqual(verdict, "REQUEST_CHANGES")
class TestVerdictBoundaries(unittest.TestCase):
"""裁决判定树decide_verdict边界与 thresholds pass=85 / request_changes=60 一致。"""
def test_pass_threshold_inclusive(self):
self.assertEqual(decide_verdict(85, False, DEFAULT_POLICY), "PASS")
def test_comment_band(self):
self.assertEqual(decide_verdict(60, False, DEFAULT_POLICY), "COMMENT")
self.assertEqual(decide_verdict(84, False, DEFAULT_POLICY), "COMMENT")
def test_request_changes_below_band(self):
self.assertEqual(decide_verdict(59, False, DEFAULT_POLICY), "REQUEST_CHANGES")
def test_hard_gate_short_circuits_high_score(self):
# 即便满分,硬门禁失败也直接 REQUEST_CHANGES
self.assertEqual(decide_verdict(100, True, DEFAULT_POLICY), "REQUEST_CHANGES")
if __name__ == "__main__":
unittest.main(verbosity=2)