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 <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-08-22 23:47:57 +08:00
parent 90db40982e
commit 707327eff9
4 changed files with 29 additions and 9 deletions
+16
View File
@@ -5371,4 +5371,20 @@
- 复发自:BUG-305thinking 与正文抢 `max_tokens`)、BUG-3468192 预算不变就重新打开 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-309validate 跳过 `next build`publish 才暴露类型错误)
- 修复版本:本提交
+10 -6
View File
@@ -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;
}
}
+1 -1
View File
@@ -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);
}
@@ -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);