From 707327eff997ee30735d9cedd637ac0291447078 Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Sat, 22 Aug 2026 23:47:57 +0800 Subject: [PATCH] fix(web): unblock staging next build after declared-window types BUG-353 fallbacks used undefined window columns and un-narrowed clock/focus values, so Docker next build failed typecheck and could not publish the already-pushed staging head. Co-authored-by: Cursor --- docs/BUG_HISTORY.md | 16 ++++++++++++++++ frontend/src/app/api/account/route.ts | 16 ++++++++++------ frontend/src/lib/declared-birth-window.ts | 2 +- .../rectification-agentic/v9/method-followup.ts | 4 ++-- 4 files changed, 29 insertions(+), 9 deletions(-) diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 6ffbb9a5..8ce99414 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -5371,4 +5371,20 @@ - 复发自:BUG-305(thinking 与正文抢 `max_tokens`)、BUG-346(8192 预算不变就重新打开 thinking) - 修复版本:`0ca7da99` +## BUG-355 | staging web 镜像 next build 被 declared window 类型检查挡住 + +- 状态:resolved +- 首次发现:2026-08-22 +- 最近更新:2026-08-22 +- 影响面:`deploy/railway-web.Dockerfile` 的 `RUN npm run build`、`GET/PATCH /api/account`、declared window 类型收口 +- 用户现象:xiaoxin 不可用时本机按 publish 合同构建 web 镜像,`next build` 在 TypeScript 阶段失败,staging 无法换上已推送的 `90db4098`。 +- 触发条件:当前 `staging` 头包含 BUG-353 的列缺失回退,且走 Docker `next build`。push 上的 validate 跳过 `npm run build`,因此测试绿、镜像红。 +- 根因:列缺失回退把 `declared_window_start/end` 写成 `undefined` 或省略,对不上 supabase 行类型。`isBirthClockText` 不是类型谓词,收窄后仍是 `string | null | undefined`。`Boolean(focus) && focus.intent` 不能收窄 `focus`。 +- 修复:缺失列回退写成 `null`。`isBirthClockText` 改为 `value is string`。焦点判断改为 `focus && (...)` 后再使用 `focus.intent`。 +- 验证:`npx tsc --noEmit` 通过。`tsx --test tests/declared-birth-window.test.ts tests/profile-persistence.test.ts` 14/14。 +- 防复发:账户列缺失回退必须补齐 declared window 字段为 `null`,不得用 `undefined`。staging push 的类型回归仍只在 publish Docker 暴露,本机/xiaoxin 镜像构建失败不得改 SHA 蒙混上线。 +- 相关记录:BUG-309、BUG-353、BUG-354 +- 复发自:BUG-309(validate 跳过 `next build`,publish 才暴露类型错误) +- 修复版本:本提交 + diff --git a/frontend/src/app/api/account/route.ts b/frontend/src/app/api/account/route.ts index 65408dfa..48915dd9 100644 --- a/frontend/src/app/api/account/route.ts +++ b/frontend/src/app/api/account/route.ts @@ -62,8 +62,8 @@ export async function GET() { if (!withoutDeclaredWindow.error && withoutDeclaredWindow.data) { profile = { ...withoutDeclaredWindow.data, - declared_window_start: undefined, - declared_window_end: undefined, + declared_window_start: null, + declared_window_end: null, }; profileError = withoutDeclaredWindow.error; } else if (withoutDeclaredWindow.error && isMissingProfileColumn(withoutDeclaredWindow.error)) { @@ -74,6 +74,8 @@ export async function GET() { .single(); profile = fallback.data ? { ...fallback.data, + declared_window_start: null, + declared_window_end: null, birth_place_label: undefined, birth_place_type: undefined, birth_place_provider: undefined, @@ -83,7 +85,7 @@ export async function GET() { } : null; profileError = fallback.error; } else { - profile = withoutDeclaredWindow.data; + profile = null; profileError = withoutDeclaredWindow.error; } } @@ -199,8 +201,8 @@ export async function PATCH(request: Request) { if (!withoutDeclaredWindow.error && withoutDeclaredWindow.data) { currentProfile = { ...withoutDeclaredWindow.data, - declared_window_start: undefined, - declared_window_end: undefined, + declared_window_start: null, + declared_window_end: null, }; currentProfileError = withoutDeclaredWindow.error; } else if (withoutDeclaredWindow.error && isMissingProfileColumn(withoutDeclaredWindow.error)) { @@ -211,6 +213,8 @@ export async function PATCH(request: Request) { .maybeSingle(); currentProfile = fallback.data ? { ...fallback.data, + declared_window_start: null, + declared_window_end: null, latitude: undefined, longitude: undefined, timezone_offset: undefined, @@ -223,7 +227,7 @@ export async function PATCH(request: Request) { } : null; currentProfileError = fallback.error; } else { - currentProfile = withoutDeclaredWindow.data; + currentProfile = null; currentProfileError = withoutDeclaredWindow.error; } } diff --git a/frontend/src/lib/declared-birth-window.ts b/frontend/src/lib/declared-birth-window.ts index 4dc0b9ca..a0d7240b 100644 --- a/frontend/src/lib/declared-birth-window.ts +++ b/frontend/src/lib/declared-birth-window.ts @@ -51,7 +51,7 @@ export function isDeclaredBirthPeriod(value: string | null | undefined): value i || value === "late_night"; } -export function isBirthClockText(value: string | null | undefined): boolean { +export function isBirthClockText(value: string | null | undefined): value is string { return typeof value === "string" && CLOCK.test(value); } diff --git a/frontend/src/lib/rectification-agentic/v9/method-followup.ts b/frontend/src/lib/rectification-agentic/v9/method-followup.ts index fa7eeadf..a4c4d3f1 100644 --- a/frontend/src/lib/rectification-agentic/v9/method-followup.ts +++ b/frontend/src/lib/rectification-agentic/v9/method-followup.ts @@ -450,8 +450,8 @@ export function buildMethodFollowupPlan(input: { const sessionOutcome = input.sessionOutcome ?? "collect_evidence"; const focus = input.activeFocus ?? null; - const keepAcceptedFocus = Boolean(focus) && ( - focus.intent === "reverse_verify" || focus.intent === "out_of_sample_check" + const keepAcceptedFocus = Boolean( + focus && (focus.intent === "reverse_verify" || focus.intent === "out_of_sample_check"), ); if (focus && (!input.accepted || keepAcceptedFocus)) { const existingChoice = parseAgentChoiceCopy(focus.expectedAnswerSchema ?? null);