Jev Git 工作流教程:提交、PR 与 CI 门禁
在 Git 提交、Pull Request 和 CI 流程中使用 Jev 完成有界语义判断,同时把 push、merge 与分支保护权限留在确定性代码和人工审批中。
1. 先划清 Jev 的职责边界
只把精确 Git 数据无法直接回答的问题交给 Jev,例如变更分类、评审领域、风险等级或是否需要人工复核。文件存在、测试结果、权限、必要评审人和受保护分支规则仍由确定性代码检查。本教程不会让模型执行 push、merge、tag、release 或绕过审批。
2. 创建一个零依赖的 PR 判断脚本
示例只使用 Node 20 内置能力和官方 POST /v1/systemone 接口。本地默认读取 staged diff;CI 通过 JEV_BASE 指向 PR 基准分支。脚本限制补丁长度、记录是否截断,并且不会发送仓库凭据。
import { appendFileSync } from 'node:fs';
import { execFileSync } from 'node:child_process';
const apiKey = process.env.TYPESAFE_API_KEY;
if (!apiKey) throw new Error('TYPESAFE_API_KEY is required');
const base = process.env.JEV_BASE?.trim();
const range = base ? [base + '...HEAD'] : ['--cached'];
const git = (...args) => execFileSync('git', args, {
encoding: 'utf8', maxBuffer: 2_000_000,
});
const names = git('diff', '--name-status', ...range);
const rawDiff = git('diff', '--unified=1', '--no-ext-diff', ...range);
if (!rawDiff.trim()) {
console.log('No changes to review.');
process.exit(0);
}
const sensitivePath = /(^|\t)(\.env($|\.)|.*\.(pem|key|p12)$)/m;
if (sensitivePath.test(names)) {
throw new Error('Refusing to send a diff containing a sensitive path');
}
const MAX_DIFF = 14_000;
const state = {
comparison: base ? base + '...HEAD' : 'staged changes',
changed_files: names.trim().split('\n').slice(0, 200),
patch_excerpt: rawDiff.slice(0, MAX_DIFF),
patch_truncated: rawDiff.length > MAX_DIFF,
};
const questions = {
change_type: {
type: 'choice',
instructions: 'What is the primary purpose of this Git change?',
criteria: {
feature: 'Adds user-visible behavior',
fix: 'Corrects broken or incorrect behavior',
maintenance: 'Refactor, dependency, documentation, or tooling',
unknown: 'Evidence is insufficient or does not fit another option',
},
},
risk: {
type: 'score',
instructions: 'How risky is this change to merge?',
criteria: [
'Low: isolated and easy to reverse',
'Moderate: touches shared behavior but has bounded impact',
'High: affects data, auth, payments, deployment, or broad behavior',
'Critical: credible irreversible or security-sensitive impact',
],
},
needs_human_review: {
type: 'noul',
instructions: 'Does this change require deliberate human review before merge?',
criteria: {
true: 'Ambiguous, high-impact, security-sensitive, or difficult to reverse',
false: 'Low-impact, well-bounded, and straightforward to reverse',
},
},
};
const response = await fetch('https://api.typesafe.ai/v1/systemone', {
method: 'POST',
headers: {
authorization: 'Bearer ' + apiKey,
'content-type': 'application/json',
},
body: JSON.stringify({ model: 'jev-latest', state, questions }),
signal: AbortSignal.timeout(15_000),
});
if (!response.ok) {
throw new Error('Jev request failed: ' + response.status);
}
const result = await response.json();
const { change_type: type, risk, needs_human_review: review } = result.answers ?? {};
const allowedTypes = new Set(Object.keys(questions.change_type.criteria));
if (!type || !allowedTypes.has(type.choice)) throw new Error('Invalid Choice answer');
if (!risk || !Number.isFinite(risk.score) || risk.score < 0 || risk.score > 3) {
throw new Error('Invalid Score answer');
}
if (!review || !Number.isFinite(review.noul) || review.noul < 0 || review.noul > 1) {
throw new Error('Invalid Noul answer');
}
// Example thresholds only. Calibrate them against your repository history.
const outcome = review.noul >= 0.65 || risk.score >= 2
? 'HUMAN REVIEW RECOMMENDED'
: 'REPORT ONLY';
const report = [
'## Jev PR decision report',
'- Outcome: **' + outcome + '**',
'- Change type: **' + type.choice + '**',
'- Type confidence: **' + type.confidence.toFixed(2) + '**',
'- Risk score: **' + risk.score.toFixed(2) + ' / 3**',
'- P(human review): **' + review.noul.toFixed(2) + '**',
'- Diff truncated: **' + state.patch_truncated + '**',
].join('\n');
console.log(report);
if (process.env.GITHUB_STEP_SUMMARY) {
appendFileSync(process.env.GITHUB_STEP_SUMMARY, report + '\n');
} 为什么直接使用 HTTP:它与官方快速开始中的请求格式一致,示例不需要安装或猜测某个社区 SDK 的版本。Node 20 已内置 fetch 和 AbortSignal.timeout。
3. 一次请求询问多个封闭问题
脚本对同一 State 同时发送三个独立问题:Choice 判断变更类型,Score 输出有序风险,Noul 判断是否需要人工复核。TypeSafe 官方说明同一请求中的问题彼此独立,因此收到结果后仍要由应用策略组合答案。
Choicechange_type只能返回 feature、fix、maintenance 或 unknown。
Scorerisk沿四级风险量表返回可落在等级之间的分数。
Noulneeds_human_review返回命题为真的概率;它没有单独的 confidence。
4. 校验响应后再执行策略
类型化结果仍需要运行时校验:Choice 必须属于原始选项,Score 必须是有限数且位于声明区间,Noul 必须在 0 到 1 之间。校验通过后再映射到清晰可见的策略,不要把权限隐藏在提示词里。
| 情况 | 只报告阶段 | 校准后的门禁 |
|---|---|---|
| API 超时或响应无效 | 记录 provider_failure,CI 继续 | 按团队策略转人工,不自动放行 |
| 风险高或复核概率高 | 在 Job Summary 中突出显示 | 要求已有的人工审批规则 |
| 风险低 | 仍然运行测试和普通评审 | 不代表允许自动合并 |
5. 先在本地以只报告模式运行
先读取暂存区,让开发者在提交前核对模型看到的证据。此阶段只输出报告、不阻止提交,用它暴露超大 diff、含糊标签、敏感路径和问题设计缺陷。
# Review exactly what is staged for the next commit.
export TYPESAFE_API_KEY='your-server-side-key'
node scripts/jev-pr-review.mjs
# Inspect the same range a pull request would use.
git fetch origin main
JEV_BASE=origin/main node scripts/jev-pr-review.mjs 6. 接入只读的 GitHub Actions
工作流拉取完整历史、获取 PR 基准分支、运行同一脚本,并把结果写入 Job Summary。权限保持只读,TYPESAFE_API_KEY 存在 Actions Secret。先报告还能积累后续校准所需的数据。
name: Jev PR report
on:
pull_request:
types: [opened, synchronize, reopened]
permissions:
contents: read
jobs:
review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Fetch base branch
run: git fetch origin "${{ github.base_ref }}"
- name: Build Jev report
env:
TYPESAFE_API_KEY: "${{ secrets.TYPESAFE_API_KEY }}"
JEV_BASE: "origin/${{ github.base_ref }}"
run: node scripts/jev-pr-review.mjs 7. 用历史 PR 校准后再启用门禁
回放带标注的历史 Pull Request,统计错误放行、无效复核、覆盖率、延迟和 provider 失败。阈值应来自真实错误成本,而不是直觉。后续即便改成阻断,也只应对团队已经验证过的特定高风险条件 fail closed。
- 至少准备一批人工标注的历史 PR,并保留低、中、高风险样本。
- 记录问题版本、模型版本、完整分布、阈值、最终人工结论和覆盖原因。
- 分别统计错误放行和过度拦截;两者成本通常不同。
- 先运行只报告模式,再要求人工复核;不要从第一天自动合并。
- 为 API 故障、超大 diff、敏感文件和 State 截断设置明确兜底。