fix(rectification): keep offer-message defined through next build
Independent Staging Quality Gate / validate (push) Successful in 10m12s
Independent Staging Quality Gate / publish (push) Successful in 13m52s

Boolean() does not narrow find(), so Docker next build failed typecheck on the selection-card key.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-08-20 01:24:59 +08:00
parent a732ff4b58
commit 464bc33202
3 changed files with 28 additions and 6 deletions
+17 -1
View File
@@ -4713,7 +4713,7 @@
- 防复发:`selection_allowed` 不得单独出示采用卡。卡片必须等本轮 `rectification-offer-candidates`。有 `next_followup` 时不得 offer。
- 相关记录:BUG-120、BUG-304、BUG-312
- 复发自:BUG-120(过早出示采用卡);BUG-312(用 `selectionAllowed` 当出示门)
- 修复版本:待提交
- 修复版本:`a732ff4b`
## BUG-314 | 生时纠正宫位表比 Agent 正文更宽、更靠左
@@ -4729,4 +4729,20 @@
- 防复发:宫位表左缘和宽度必须跟 Agent 正文列一致;inset 只加在 snapshot 容器上。
- 相关记录:BUG-310、BUG-311
- 复发自:BUG-311(去掉消息缩进后未收回正文列)
- 修复版本:`a732ff4b`
## BUG-315 | Docker `npm run build` 因候选卡消息可能为 undefined 类型检查失败
- 状态:resolved
- 首次发现:2026-08-20
- 最近更新:2026-08-20
- 影响面:`deploy/railway-web.Dockerfile``RUN npm run build`、生时纠正时间选择卡
- 用户现象:staging publish 在 web 镜像构建失败。BuildKit 日志末尾是 `skill-package-registry.ts` 的 Import traces,真正失败是 `Failed to type check``latestOfferMessageKey` 可能为 `undefined`
- 触发条件:向 `staging` 推送后走 publish 镜像构建。push 上的 validate 跳过 `npm run build`,因此质量门绿、镜像红。
- 根因:`showSelectionCards``Boolean(...)` 包了一层,TypeScript 不会因此收窄 `find()``T | undefined`。随后 `showSelectionCards ? latestOfferMessageKey.renderKey` 在 true 分支仍可能是 `undefined``next build` 类型检查退出 1。
- 修复:变量改名为实际类型 `latestOfferMessage`;取 `renderKey` 时用 `showSelectionCards && latestOfferMessage` 收窄。
- 验证:`./node_modules/.bin/tsc --noEmit` 无错误。`tsx --test tests/rectification-agentic-entry.test.ts` 锁定收窄写法。
- 防复发:对 `find()` 结果取属性不得只靠外层 `Boolean(...)`staging push 的 validate 不跑 `next build`,类型回归只在 publish Docker 暴露。
- 相关记录:BUG-309、BUG-312、BUG-313
- 复发自:BUG-309validate 跳过 `next build`;日志末尾 Import traces 掩盖类型检查失败)
- 修复版本:待提交
@@ -549,7 +549,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
&& Boolean(message.turnId)
&& Boolean(message.text)
))?.renderKey;
const latestOfferMessageKey = [...messages]
const latestOfferMessage = [...messages]
.reverse()
.find((message) => (
message.role === "assistant"
@@ -559,12 +559,14 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
));
const showSelectionCards = Boolean(
candidateResult?.selectionAllowed
&& latestOfferMessageKey
&& turnOfferedSelection(latestOfferMessageKey)
&& latestOfferMessage
&& turnOfferedSelection(latestOfferMessage)
&& !busy
&& regeneratingMessageKey === null,
);
const selectionCardMessageKey = showSelectionCards ? latestOfferMessageKey.renderKey : undefined;
const selectionCardMessageKey = showSelectionCards && latestOfferMessage
? latestOfferMessage.renderKey
: undefined;
const canSend = !busy && !readonly && !regeneratingMessageKey;
return (
@@ -367,7 +367,11 @@ test("time-selection cards appear under the latest settled agent bubble only aft
assert.match(chat, /candidateResult\?\.selectionAllowed/);
assert.match(chat, /turnOfferedSelection/);
assert.match(chat, /rectification-offer-candidates/);
assert.match(chat, /showSelectionCards = Boolean\(\s*candidateResult\?\.selectionAllowed[\s\S]*turnOfferedSelection\(latestOfferMessageKey\)[\s\S]*!busy/);
assert.match(chat, /showSelectionCards = Boolean\(\s*candidateResult\?\.selectionAllowed[\s\S]*turnOfferedSelection\(latestOfferMessage\)[\s\S]*!busy/);
assert.match(
chat,
/selectionCardMessageKey = showSelectionCards && latestOfferMessage\s*\? latestOfferMessage\.renderKey\s*: undefined/,
);
assert.match(messageLoop, /showSelectionCards && message\.renderKey === selectionCardMessageKey/);
assert.doesNotMatch(messageLoop, /className="rectification-snapshot"/);
});