feat(guard): E_WAIT_LANGUAGE 补第四类——把「什么时候上线」写成派给前端的动作项
changelog-filename-gate / validate (push) Failing after 1s

今天上午装这条守卫时只想到「等/待/另发」这一种形态,当天下午就被另一种形态绕过去了:
20_7443 正文写着「上生产前请与后端确认这个开关的状态」与「生产环境未开」,
mmg 据此来问上线时间、并要求「后端把生产开关打开」——而守卫全绿,因为这两句
一个词表词都没用上。它们把不确定性包装成了「请你去确认」,语法换了,作用一样:
读者只能停在那里等一个他查不到的状态。

判据仍是那一句:这条影响他「怎么写代码」,还是只影响他「什么时候开始写」。
上线时点属后者。前端需不需要同步上线,由 frontend_action_required 与模板里
「前端是否必须同步上线」那个结构化字段承载,正文自由文本里不该再出现。

词表先对全仓 1068 份 changelog 实跑,只留零命中且零正当用法的 12 个词。剔除两个:
  「何时开」  —— 误伤「保护何时开始生效」「窗口何时开过」
  「生产上线」—— 误伤 07_5640「生产上线需配 annual-direct-plan-id」,那是真契约边界

部署时间戳没做成规则:该形态全仓 0 命中,分辨力无从验证,而必须放行的
「带时刻实测取证句」有 690 处——判据的误伤面远大于收益时,门禁只会教人绕开它。
这一类只能靠 §2.1 的条文和复盘接住,机器接不住,如实记在注释里。

测试:新增 2 条阳性 + 1 条阴性对照,阴性那条与阳性只差一个「上生产前」,
用来钉住分界线(「收到 809009 找后端确认该环境的开关」是运维处置,必须放行)。
npm test 59 项 58 绿;唯一的红是存量的 E_ALIAS_STATE(11_7510,与本次无关)。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
这个提交包含在:
API Changelog Bot
2026-09-21 16:23:33 +08:00
共同撰写人 Claude Opus 5
父节点 4fbf5ec2c0
当前提交 78ca3f1ee3
共修改 2 个文件,包含 48 行新增和 0 行删除
@@ -445,6 +445,21 @@ const WAIT_LANGUAGE_PHRASES = [
// 三、直接叫停对接
'请先等', '暂缓对接', '暂不要对接', '先不要对接', '先别对接',
'我们这边还在', '该缺陷已在修',
// 四、把「什么时候上线」写成派给前端的动作项
// (2026-09-21 复盘补:20_7443 写了「上生产前请与后端确认这个开关的状态」,
// mmg 因此来问上线时间与「请后端把生产开关打开」。上面三类词表一个都没拦住——
// 它们匹配的是「等/待/另发」,而这句把不确定性包装成了「请你去确认」。
// 上线时点不影响「怎么写代码」,只影响「什么时候开始写」,属本规则该删的那一半。
// 前端需不需要同步上线,有 frontend_action_required 与模板里「前端是否必须同步上线」
// 那个结构化字段承载,正文自由文本里不再重复。)
'上生产前请', '上生产前与', '上线前请', '上线前与',
'上线时间', '上线排期', '上生产时间', '何时上线',
'由谁开', '谁来开', '排期确定后', '排期确认后',
// ⚙ 词表已对全仓 1068 份 changelog 实跑,以上全部零命中。两个候选词被剔除:
// 「何时开」——误伤「保护何时开始生效」「窗口何时开过」
// 「生产上线」——误伤 07_5640「生产上线需配 annual-direct-plan-id」(真契约边界)
// 部署时间戳没做成规则:该形态全仓 0 命中(分辨力验不了),
// 而必须放行的「带时刻实测取证句」有 690 处,误伤面远大于收益。
// ⚠️ 不要把「暂不可用 / 暂时不可用」加回来:2026-09-21 对全仓 1968 份 changelog 实测,
// 它命中的几乎全是错误码表与响应示例里的**文案**(如「584105 结算字典暂时不可用,请稍后重试」),
// 那正是本规则必须放行的契约内容。判据的分辨力不够时,门禁只会教人绕开它。
@@ -187,6 +187,39 @@ test('accepts coverage limits that do not ask the frontend to wait', () => {
assert.deepEqual(errors.filter(({ code }) => code === 'E_WAIT_LANGUAGE'), []);
});
test('rejects turning launch timing into an action item for the frontend', () => {
const value = document().replace('## 七、不影响范围', `## 六.8、环境开关
- 上生产前请与后端确认这个开关的状态,否则提交会被拒。
## 七、不影响范围`);
const errors = validateV2Document(FILE, value, { requireV2: true });
assert.ok(errors.some(({ code }) => code === 'E_WAIT_LANGUAGE'));
});
test('rejects asking who flips the production switch and when', () => {
const value = document().replace('## 七、不影响范围', `## 六.8、环境开关
- 生产开关由谁开、何时上线,待排期确认后另行同步。
## 七、不影响范围`);
const errors = validateV2Document(FILE, value, { requireV2: true });
assert.ok(errors.some(({ code }) => code === 'E_WAIT_LANGUAGE'));
});
// 阴性对照:撞上错误码时的运维处置建议是「怎么诊断」,不是「什么时候开工」,必须放行。
// 它与上一条只差一个「上生产前」,正好钉住本规则的分界线。
test('accepts runbook advice for an error code without launch timing', () => {
const value = document().replace('## 七、不影响范围', `## 六.8、环境开关
- 收到 \`809009\` 即表示所在环境的开关没开,找后端确认该环境的开关——这不是数据问题,重试与补数据均无效。
- 窗口何时开过、保护何时开始生效,都可从时间线事件读到。
## 七、不影响范围`);
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,