Merge pull request 'feat(examples): add pr-quality-gatekeeper end-to-end workflow(子题三:门禁闭环 + 全仓批扫)' (#219) from recorder/gitlink-cli:feat/example-workflow-pr-gatekeeper into master
This commit is contained in:
commit
d3485fce45
|
|
@ -0,0 +1,3 @@
|
|||
outputs/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
|
@ -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` 策略算出**确定性 0–100 评分卡**与**三态裁决**(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 | 本仓库真实 PR(pull_request_id 15222) | ✅ PASS 90/100,8 个变更文件路由正确 |
|
||||
| 注入真实审查发现 | 同一 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.5;96% 未关联 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)。
|
||||
|
|
@ -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))。
|
||||
|
|
@ -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 API;Python 3.9+ 与 gitlink-cli 的安装方式按实际
|
||||
# runner 镜像调整(下面 install 步骤仅为占位示意)。
|
||||
#
|
||||
# 触发:对 PR 的 open / 同步事件运行门禁,脚本返回码 2(REQUEST_CHANGES)
|
||||
# 会让本 job 失败,从而在 CI 上挡住该 PR(PASS/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/
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
# config.example.yaml — PR 看门人闭环的工作流配置(gatekeeper_workflow.py --config 用)
|
||||
#
|
||||
# 命令行参数(--owner/--repo/--pr/--policy/--owner-rules/--findings)会覆盖这里的同名字段。
|
||||
# 相对路径以本配置文件所在目录为基准解析。
|
||||
|
||||
owner: Gitlink # 目标仓库 owner(GitLink 主分支为 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
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
# 架构与数据流 — PR 看门人闭环
|
||||
|
||||
本工作流采用「**采集 → 路由 → 裁决 → 回写/善后**」四段式流水线,把 `gitlink-gatekeeper` 的 Policy-as-Code 门禁包成一条可复现闭环。所有数值/字段/算法以已收录的 [`gitlink-gatekeeper` Skill REFERENCE](../../../../skills/gitlink-gatekeeper/REFERENCE.md) 为准。
|
||||
|
||||
## 设计目标
|
||||
|
||||
- **可复现**:同策略 + 同 PR 上下文 → 同评分卡 + 同裁决(确定性算法,SSOT 第 3–5 节)。
|
||||
- **可审计**:评分卡逐维列分 + 备注,硬门禁逐条列出命中原因,裁决可追溯到具体规则与 `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)
|
||||
- 五维加权评分(权重和=100,SSOT 第 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. 形成从「数据获取」到「治理动作落地」的端到端闭环,并能接入 CI(REQUEST_CHANGES 返回码 2)。
|
||||
3. 复用本作品自研的 `label` 命令组(子赛题一)与 gatekeeper 策略(子赛题二),三个子赛题在同一作品内闭环,相互增强。
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
# 最短复现路径(3 步)
|
||||
|
||||
## 1. 安装与认证
|
||||
|
||||
```bash
|
||||
npm install -g @gitlink-ai/cli # ≥0.2.0(label 命令与 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 Actions,PR 触发自动门禁)。
|
||||
- **改门禁松紧**:复制一份 `gatekeeper.yaml` 改 `weights/hard_gates/thresholds`,字段说明见 [Skill REFERENCE](../../../../skills/gitlink-gatekeeper/REFERENCE.md)。
|
||||
|
||||
## 验证自己改动没破坏确定性
|
||||
|
||||
```bash
|
||||
python3 tests/test_scoring.py # 同输入 → 同分 → 同裁决 的回归护栏
|
||||
```
|
||||
|
|
@ -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. 运行步骤
|
||||
|
||||
### 步骤 A:dry-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`(其余忽略)。
|
||||
|
||||
### 步骤 C:apply 执行写操作
|
||||
|
||||
确认 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` | 工作流配置 YAML(owner/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`。
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
# 真实平台验证记录
|
||||
|
||||
> 全部针对 **GitLink 线上真实平台** 运行(`gitlink-cli` + Token 认证),非 mock。
|
||||
> 他人仓库一律 dry-run(只读零写入);写操作只在自有 fork 演练。
|
||||
> 运行环境:macOS(Apple Silicon)· Python 3.9 · `@gitlink-ai/cli` 0.2.0(npm 官方发布版,零本地构建)。
|
||||
|
||||
## A. dry-run:真实 PR → PASS 90/100
|
||||
|
||||
对本仓库真实 PR(`pull_request_id 15222`,feat/org-team-projects,8 个变更文件):
|
||||
|
||||
- 路由正确分流:README/docs/skill → doc-maintainer;`org.go` → go-reviewer;`org_test.go` → qa-reviewer
|
||||
- 评分(确定性,可手算复现):review 40/40 · test 20/20(1 src/1 test)· hygiene 10/15(desc✓/issue✗/size✓)· commit 15/15 · ci 5/10(unknown)= **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/40(1 major + 2 minor),总分 90 → 55,**裁决翻转为 REQUEST_CHANGES**。
|
||||
|
||||
**发现是真的,不是编的**——三条均来自对该 PR 真实 diff(head `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. 评分卡评论回写到 PR(comment id `472741`)
|
||||
2. 自动创建 tracking issue(id `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 起官方发布版自带,整条闭环零本地构建跑通。
|
||||
|
|
@ -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 中 action(add_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*
|
||||
|
|
@ -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*
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
# gatekeeper 仓库体检报告 —— Gitlink/gitlink-cli(2026-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 得到同样的分数与裁决。
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"_comment": "对 Gitlink/gitlink-cli PR #15222(feat/org-team-projects,head 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 中 action(add_all_team_projects/remove_all_team_projects)与实际请求 path 段(create_all/destroy_all,见 :134)是两套词汇,排查问题时易误导;建议统一或在 payload 同时给出两者映射"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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"]
|
||||
|
|
@ -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 审查发现(--findings),review_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
|
|
@ -0,0 +1,248 @@
|
|||
#!/usr/bin/env python3
|
||||
"""可复现评分单测 —— 把「同输入 → 同分 → 同裁决」从口号变成可验证事实。
|
||||
|
||||
纯标准库 unittest(Python 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/10;severity_penalty blocker=100/major=25/minor=5/nit=1;
|
||||
thresholds pass=85 / request_changes=60;max_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 维度计 len,src/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.md:3 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.md:4 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.md:2 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.md:4 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)
|
||||
Loading…
Reference in New Issue