feat(gate): 新增 E_WAIT_LANGUAGE——交接件正文禁「让前端等我们」的措辞
changelog-filename-gate / validate (push) Failing after 2s
changelog-filename-gate / validate (push) Failing after 2s
2026-09-21 wx 第二次点名:「不要在changelog里写让前端等待部署 这不是你第一回犯错了 工作流是你部署完测试环境推送changelog」。 §2.1 的 backend_status 门禁挡得住预告式推送,挡不住这一类:20_7443 的 frontmatter 已经是 deployed、CI 全绿,但正文 status_note 结尾写着「该缺陷已在修……修好后另发 交接件」。mmg 因此一直没动工,隔天才来问「这个是有啥问题吗 还是没做到呢」——门禁 只看 frontmatter,看不见自由文本里的这句话,所以「deployed + 校验绿」并不代表这份 交接件可执行。消费方也没有能力消解这种不确定性:他查不了我们的部署状态、看不到 dev-v3、不知道「另发」是哪天,读到「等」就只能等,而且是静默地等。 - scripts/validate-changelog-frontmatter.mjs:新增 validateNoWaitLanguage,对所有 v2 文档逐行扫描(与 change_type 无关,前端条目同样适用)。三类措辞:部署状态对冲、 未来交付承诺、直接叫停对接;「等待」做共现判定而非裸词匹配,避免把「前端需轮询 等待支付回调」这类业务语义一起拦掉。前向引用(「以后续订正为准」「见后续订正」) 一并封住——它和「修好后另发」是同一件事换个说法,实测被绕过一次。 - tests:4 个用例,含 1 个阴性对照(灰度开关状态、已知缺口工单号、业务流程里的等待 必须放行),防止作者为了过门禁把该写的契约边界一起删掉。 - BACKEND_CHANGELOG_DELIVERY_GUIDE.md §2.1:写明规则、背景与那条分界线——这条影响 他「怎么写代码」,还是只影响他「什么时候开始写」?后者一律删。 阳性对照:对已推送的 HEAD 版 20_7443 跑新规则,命中 2 处(第 308、451 行「修好后另发」), 即它能抓住真实发生过的那次。既有 changelog-path-aliases 测试对 11_7510 的 2 条 E_ALIAS_STATE 红是本次改动之前就存在的,与本提交无关,pre-push 钩子也不跑该用例。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
这个提交包含在:
@@ -423,6 +423,69 @@ export function validateFrontendTransition(current, target, reason = '') {
|
||||
return [`禁止跨级迁移: ${current} -> ${target}`];
|
||||
}
|
||||
|
||||
// 🔴 E_WAIT_LANGUAGE(2026-09-21 wx 第二次点名:「不要在changelog里写让前端等待部署 这不是你第一回犯错了」)
|
||||
// 背景:20_7443 的 frontmatter 已是 backend_status: deployed、CI 全绿,但正文 status_note 结尾写着
|
||||
// 「该缺陷已在修……修好后另发交接件」。mmg 因此一直没动工,直到隔天来问「这个是有啥问题吗 还是没做到呢」。
|
||||
// 门禁只校验 frontmatter,看不见自由文本里的这句话——「deployed + 校验绿」并不代表这份交接件可执行。
|
||||
// 消费方也没有能力消解这种不确定性:他查不了我们的部署状态、看不到 dev-v3、不知道「另发」是哪天,
|
||||
// 读到「等」就只能等,而且是静默地等(文件推了、看起来已交付,唯一的异常信号是有人在安静空转)。
|
||||
// 正确顺序是「部署测试环境 → 实测验证 → 再推 changelog」,把不确定性关掉,而不是写进正文交给前端。
|
||||
// ⚠️ 本规则只拦「我方在制品」,不拦「契约自身的覆盖边界」:
|
||||
// 该写 —— 灰度开关状态、前置字段要求、会抛的错误码、已知缺口的工单号(前端照样能开工,只是知道边界在哪)
|
||||
// 不该写 —— 我们还没部署 / 还在修 / 稍后另发(前端只能停手)
|
||||
// 分界线:这条影响他「怎么写代码」,还是只影响他「什么时候开始写」?后者一律删掉。
|
||||
const WAIT_LANGUAGE_PHRASES = [
|
||||
// 一、部署状态对冲
|
||||
'等部署', '待部署', '未部署', '部署后再', '部署完再', '部署完成后再', '是否已部署',
|
||||
'未滚动到测试', '待后端部署', '后端部署后', '部署状态未核', '部署状态未知',
|
||||
'自行确认部署', 'deploy-status.sh 确认',
|
||||
// 二、未来交付承诺(兑现时点前端判断不了,只能等)
|
||||
'稍后另发', '修好后另发', '另发交接件', '后续补发', '另行补发', '后续再发', '届时再发',
|
||||
'后续订正', '以后续', '见后续', '后续另', '另行通知', '再行通知', '进度以后', '进展以后',
|
||||
// 三、直接叫停对接
|
||||
'请先等', '暂缓对接', '暂不要对接', '先不要对接', '先别对接',
|
||||
'暂不可用', '暂时不可用', '我们这边还在', '该缺陷已在修',
|
||||
];
|
||||
|
||||
// 「等待」本身在业务描述里是合法的(如「前端需轮询等待支付回调」),只有当它指向我方在制品时才算违规。
|
||||
// 所以对它做共现判定,而不是裸词匹配——裸词会逼作者为了过门禁把该写的业务语义一起删掉。
|
||||
const WAIT_VERB = '等待';
|
||||
const WAIT_SUBJECT_WORDS = ['部署', '后端', '我们', '上线', '修复', '另发', '发版', '滚动'];
|
||||
|
||||
/**
|
||||
* 校验正文没有「让前端等我们」的措辞。命中即硬失败——这类句子会让消费方无限期停工,
|
||||
* 而 frontmatter 门禁在它面前是全绿的,没有任何其他信号会暴露它。
|
||||
*
|
||||
* @param {string} file 相对路径,仅用于报错定位
|
||||
* @param {string} body frontmatter 之后的正文
|
||||
* @returns {Array} 违规列表,每行最多报一条
|
||||
*/
|
||||
function validateNoWaitLanguage(file, body) {
|
||||
const errors = [];
|
||||
const lines = String(body || '').split(/\r?\n/);
|
||||
for (let index = 0; index < lines.length; index += 1) {
|
||||
const line = lines[index];
|
||||
let phrase = WAIT_LANGUAGE_PHRASES.find((candidate) => line.includes(candidate));
|
||||
if (!phrase && line.includes(WAIT_VERB)) {
|
||||
const subject = WAIT_SUBJECT_WORDS.find((word) => line.includes(word));
|
||||
if (subject) {
|
||||
phrase = `${WAIT_VERB}…${subject}`;
|
||||
}
|
||||
}
|
||||
if (!phrase) {
|
||||
continue;
|
||||
}
|
||||
errors.push(ruleError(
|
||||
'E_WAIT_LANGUAGE',
|
||||
file,
|
||||
`第 ${index + 1} 行出现让前端等待的措辞「${phrase}」:交接件只描述「已就绪的契约」`
|
||||
+ '与「前端调用时会撞上的限定」,不描述我方在制品。先部署测试环境并实测,再推 changelog;'
|
||||
+ `确实没就绪的部分整段删掉,不要写成「稍后另发」。原文:${line.trim().slice(0, 80)}`,
|
||||
));
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
export function validateV2Document(file, text, { requireV2 = false } = {}) {
|
||||
const { metadata, body, raw } = parseFrontmatter(text);
|
||||
if (!metadata) {
|
||||
@@ -497,6 +560,9 @@ export function validateV2Document(file, text, { requireV2 = false } = {}) {
|
||||
if (/\{\{[^{}\n]+\}\}|\bTODO\b|待补充/i.test(proseBody)) {
|
||||
errors.push(ruleError('E_PLACEHOLDER', file, '正文仍有 TODO、待补充或模板占位符'));
|
||||
}
|
||||
// 正文不得出现「让前端等我们」的措辞;与 change_type 无关——前端条目同样适用。
|
||||
errors.push(...validateNoWaitLanguage(file, body));
|
||||
|
||||
// 接口类条目必须逐项遵循根模板,保证前端不依赖 Swagger 或口头补充也能联调。
|
||||
if (API_CHANGE_TYPES.has(metadata.change_type)) {
|
||||
errors.push(...validateApiTemplate(file, metadata, body));
|
||||
|
||||
在新工单中引用
屏蔽一个用户