From 047a4be4fd9c0e2bc1cce73e91dd1cfd911bc1ee Mon Sep 17 00:00:00 2001 From: API Changelog Bot Date: Mon, 21 Sep 2026 15:26:59 +0800 Subject: [PATCH] =?UTF-8?q?feat(gate):=20=E6=96=B0=E5=A2=9E=20E=5FWAIT=5FL?= =?UTF-8?q?ANGUAGE=E2=80=94=E2=80=94=E4=BA=A4=E6=8E=A5=E4=BB=B6=E6=AD=A3?= =?UTF-8?q?=E6=96=87=E7=A6=81=E3=80=8C=E8=AE=A9=E5=89=8D=E7=AB=AF=E7=AD=89?= =?UTF-8?q?=E6=88=91=E4=BB=AC=E3=80=8D=E7=9A=84=E6=8E=AA=E8=BE=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- BACKEND_CHANGELOG_DELIVERY_GUIDE.md | 6 ++ scripts/validate-changelog-frontmatter.mjs | 66 +++++++++++++++++++ tests/validate-changelog-frontmatter.test.mjs | 45 +++++++++++++ 3 files changed, 117 insertions(+) diff --git a/BACKEND_CHANGELOG_DELIVERY_GUIDE.md b/BACKEND_CHANGELOG_DELIVERY_GUIDE.md index c26360b5..451bd473 100644 --- a/BACKEND_CHANGELOG_DELIVERY_GUIDE.md +++ b/BACKEND_CHANGELOG_DELIVERY_GUIDE.md @@ -64,6 +64,12 @@ verified_at: "" - 接口类条目(新增接口/修改接口/删除接口):推送前必须走完「PR 合并 → 部署测试服 → 测试服真实 API 验证」,frontmatter 必须 `backend_status: "deployed"`,并在正文「验证证据」章节贴实测结果。 - `backend_status` 为 `merged` / `pending` / `implemented` 等未部署状态的条目**禁止 push**(校验规则 E_BACKEND_PENDING 会拦)。「先给前端契约、部署随后」的预告式推送一律禁止——前端拿到 changelog 会立刻联调,接口不在等于空耗与误判。 +- 🔴 **正文里不得出现任何「让前端等我们」的措辞**(2026-09-21 wx 第二次点名,校验规则 `E_WAIT_LANGUAGE` 会拦):`等部署` / `待部署` / `未部署` / `稍后另发` / `修好后另发` / `另发交接件` / `暂不可用` / `暂缓对接` / `该缺陷已在修` / `自行确认部署`,以及「等待」与「部署/后端/我们/上线/修复/另发/发版/滚动」同行共现。 + - **为什么 §2.1 的 `backend_status` 门禁不够**:它是 frontmatter 字段,而这类句子活在自由文本里,门禁一个字也看不见——`deployed` + 校验器全绿**不等于**这份交接件可执行。2026-09-20 的 `20_7443` 就是这样:frontmatter 已 `deployed`、CI 绿,正文 `status_note` 结尾一句「该缺陷已在修……修好后另发交接件」,mmg 因此一直没动工,直到 09-21 才来问「这个是有啥问题吗 还是没做到呢」。 + - **为什么不能交给前端自己判断**:消费方查不了我们的部署状态、看不到 `dev-v3`、不知道「另发」是哪天。我方如实写下的「未核」,到他那里只剩一个可选动作——等,而且是静默地等:文件推了、门禁绿了、日报也记了,唯一的异常信号是有人在安静空转,比压根没发更难发现(没发至少还会有人来催)。**对 wx 该说「没核」,对下游只能说「能接」,或者干脆先别发。** + - **正文只允许两类内容**:①已经就绪的契约;②前端调用时会撞上的限定(灰度开关状态、前置字段要求、会抛的错误码、已知缺口的工单号)。分界线是问一句:**这条影响他「怎么写代码」,还是只影响他「什么时候开始写」?**后者一律删掉。 + - **某部分确实还没就绪时**:要么整份不发,要么把没就绪的那块**整段删掉**,只交他现在就能接的部分;绝不写成「稍后另发」。 + - ⚠️ 本规则只拦我方在制品,**不拦契约边界**——「生产环境灰度开关尚未开启(属独立运维动作)」「TRANSFER-only 订单在 9 个下游消费方无产出,已记 #8056」这类必须照写,否则就撞上「交接件要把自己的覆盖范围写在脸上」那条相反的要求;`E_WAIT_LANGUAGE` 配了阴性对照用例保证不误拦这类句子。 - 纯前端条目(前端缺陷/前端优化/前端修复):`backend_status: "not_required"`,change_type 用对应前端类型;`frontend_status: "not_required"` 时不得残留 frontend_owner / frontend_ref / target_release / verified_at。 - 背景:2026-08-06~08-07 三条未部署即推送的条目(#5599/#5567/#5633)导致前端在测试环境验不到字段(2026-08-10 投诉属实);当时仓库 CI 因校验规则假阳性长期常红被忽略,规则已于 2026-08-10 修正(前端条目类型合法化、`{orderId}` 路径参数不再误判为占位符),此后 **CI 红 = 真违规,必须当场修复回填**。 diff --git a/scripts/validate-changelog-frontmatter.mjs b/scripts/validate-changelog-frontmatter.mjs index 50c07973..6a1d9f54 100644 --- a/scripts/validate-changelog-frontmatter.mjs +++ b/scripts/validate-changelog-frontmatter.mjs @@ -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)); diff --git a/tests/validate-changelog-frontmatter.test.mjs b/tests/validate-changelog-frontmatter.test.mjs index 69da1a37..5e7facd8 100644 --- a/tests/validate-changelog-frontmatter.test.mjs +++ b/tests/validate-changelog-frontmatter.test.mjs @@ -142,6 +142,51 @@ test('requires the template author format and write-operation database section', assert.ok(errors.some(({ code }) => code === 'E_API_TEMPLATE')); }); +test('rejects a handoff that tells the frontend to wait for us', () => { + const value = document().replace('## 七、不影响范围', `## 六.8、当前状态 + +- 🔴 该缺陷已在修(属 #7990 那一族),修好后另发交接件。 + +## 七、不影响范围`); + const errors = validateV2Document(FILE, value, { requireV2: true }); + assert.ok(errors.some(({ code }) => code === 'E_WAIT_LANGUAGE')); +}); + +test('rejects forward references that only rename the waiting', () => { + const value = document().replace('## 七、不影响范围', `## 六.8、当前状态 + +- 该链路属 #7990 那一族,处理进展以后续订正为准,本节不复述内部处理状态。 + +## 七、不影响范围`); + const errors = validateV2Document(FILE, value, { requireV2: true }); + assert.ok(errors.some(({ code }) => code === 'E_WAIT_LANGUAGE')); +}); + +test('rejects hedging about whether the backend is deployed', () => { + const value = document().replace('## 七、不影响范围', `## 六.8、当前状态 + +- 已合入 dev-v3;是否已滚动到测试服未核,取证前请自行跑 deploy-status.sh 确认。 + +## 七、不影响范围`); + const errors = validateV2Document(FILE, value, { requireV2: true }); + assert.ok(errors.some(({ code }) => code === 'E_WAIT_LANGUAGE')); +}); + +// 阴性对照:契约自身的覆盖边界(灰度开关状态、已知缺口的工单号、业务流程里的等待)不是「让前端等我们」。 +// 它们影响的是「怎么写代码」而不是「什么时候开始写」,必须放行——否则作者会为了过门禁把该写的限定一起删掉, +// 那正好撞上「产出物必须把自己的覆盖范围写在脸上」那条相反的要求。 +test('accepts coverage limits that do not ask the frontend to wait', () => { + const value = document().replace('## 七、不影响范围', `## 六.8、已知边界 + +- 灰度开关 \`hl.order.requirement.transfer-kind-submit-enabled\` 在生产环境尚未开启,开启属独立运维动作。 +- TRANSFER-only 订单在 9 个下游消费方无产出,已记 #8056,不影响本次对接。 +- 支付结果异步返回,前端需轮询等待支付回调。 + +## 七、不影响范围`); + const errors = validateV2Document(FILE, value, { requireV2: true }); + assert.deepEqual(errors.filter(({ code }) => code === 'E_WAIT_LANGUAGE'), []); +}); + test('rejects backend and gateway pending at publication', () => { const errors = validateV2Document( FILE,