fix: repair missing reported birth-time status
This commit is contained in:
@@ -68,6 +68,7 @@ jobs:
|
||||
frontend/supabase/migrations/20260724030000_allow_assistant_only_rectification_regenerate.sql \
|
||||
frontend/supabase/migrations/20260725010000_structured_conversational_date_confirmation.sql \
|
||||
frontend/supabase/migrations/20260725020000_repair_structured_conversational_date_validator.sql \
|
||||
frontend/supabase/migrations/20260726010000_backfill_reported_birth_time_status.sql \
|
||||
"$DEPLOY_USER@$DEPLOY_HOST:$REMOTE_DIR/"
|
||||
|
||||
- name: Check or apply reviewed migrations
|
||||
@@ -120,7 +121,8 @@ jobs:
|
||||
"$REMOTE_DIR/20260724020000_align_global_birthplace_rectification_contract.sql" \
|
||||
"$REMOTE_DIR/20260724030000_allow_assistant_only_rectification_regenerate.sql" \
|
||||
"$REMOTE_DIR/20260725010000_structured_conversational_date_confirmation.sql" \
|
||||
"$REMOTE_DIR/20260725020000_repair_structured_conversational_date_validator.sql"
|
||||
"$REMOTE_DIR/20260725020000_repair_structured_conversational_date_validator.sql" \
|
||||
"$REMOTE_DIR/20260726010000_backfill_reported_birth_time_status.sql"
|
||||
do
|
||||
filename="$(basename "$sql_file")"
|
||||
checksum="$(sha256sum "$sql_file" | awk '{print $1}')"
|
||||
|
||||
@@ -1413,3 +1413,19 @@
|
||||
- 防复发:代码不得要求每轮必须提问,也不得把隐藏 prompt 自动拼接到 Agent 正文;没有可见短答问题时 `evidenceRequest` 应为 null,用户叙述节奏由 Agent 与用户共同决定,代码只维护记录、抽取、评分、收敛与确认边界。
|
||||
- 相关记录:BUG-070、BUG-072、BUG-074
|
||||
- 修复版本:待提交(本地可测)
|
||||
|
||||
## BUG-076 | 完整出生资料已保存但权威档案状态为空
|
||||
|
||||
- 状态:resolved
|
||||
- 首次发现:2026-07-26
|
||||
- 最近更新:2026-07-26
|
||||
- 影响面:账户出生资料保存、星盘库同步、`POST /api/consult` 服务端资料校验
|
||||
- 用户现象:出生日期、家人确认的具体时间、地点、坐标和时区都已填写,星盘库副本也显示 `birthTimeStatus: reported`,但咨询仍返回“暂时无法核对完整出生资料”。
|
||||
- 触发条件:首次创建 `profiles` 行,或既有合法出生声明的 `birth_time_status` 为空且用户原样重新保存。
|
||||
- 根因:分钟校正主链合并时把 BUG-018 的首次档案派生状态分支退回为 `{}`;共享 helper 又只在声明字段发生变化时写 `reported`,导致空状态行原样重存也无法自愈。`chart_profiles.profile` 只是星盘库 JSON 副本,不是咨询接口采用的权威出生资料。
|
||||
- 修复:账户路由统一调用共享状态派生 helper;新档案和合法声明空状态均原子写入 `reported`,确认分钟继续禁止被普通资料编辑覆盖;增加前向迁移,仅回填没有 active/candidate/case 应用结果且声明满足来源规则的空状态行。
|
||||
- 验证:账户 helper 回归覆盖新档案、空状态原样重存、候选失效和 confirmed 保护;迁移契约锁定安全过滤条件,并要求生产迁移工作流上传和应用各一次。
|
||||
- 防复发:服务端权威 `profiles` 的声明和状态必须同写;星盘库 JSON 不得作为咨询真值,历史空状态只能通过受约束前向迁移修复。
|
||||
- 相关记录:BUG-018、BUG-073
|
||||
- 复发自:BUG-018
|
||||
- 修复版本:待提交(本地可测)
|
||||
|
||||
@@ -163,9 +163,7 @@ export async function PATCH(request: Request) {
|
||||
if (currentProfileError) {
|
||||
return NextResponse.json({ error: "暂时无法核对现有出生资料" }, { status: 500 });
|
||||
}
|
||||
const applicationPatch = currentProfile
|
||||
? resolveAccountBirthTimeApplicationPatch(currentProfile, payload)
|
||||
: {};
|
||||
const applicationPatch = resolveAccountBirthTimeApplicationPatch(currentProfile, payload);
|
||||
const baseProfile = {
|
||||
id: userId,
|
||||
...(payload.name !== undefined ? { name: payload.name } : {}),
|
||||
|
||||
@@ -235,17 +235,31 @@ export type AccountBirthTimeApplicationPatch = Readonly<{
|
||||
}>;
|
||||
|
||||
export function resolveAccountBirthTimeApplicationPatch(
|
||||
current: AccountBirthTimeState,
|
||||
current: AccountBirthTimeState | null,
|
||||
patch: AccountProfilePatch,
|
||||
): AccountBirthTimeApplicationPatch {
|
||||
const declarationChanged = declarationFields.some((field) => (
|
||||
patch[field] !== undefined && patch[field] !== current[field]
|
||||
));
|
||||
if (!declarationChanged) return {};
|
||||
if (!current) {
|
||||
return patch.birth_time_source ? {
|
||||
active_birth_time: null,
|
||||
birth_time: null,
|
||||
birth_time_status: "reported",
|
||||
rectification_case_id: null,
|
||||
} : {};
|
||||
}
|
||||
|
||||
const confirmed = current.birth_time_status === "confirmed"
|
||||
|| (current.birth_time_status === null && isBirthClockTime(current.birth_time ?? ""));
|
||||
if (confirmed) return {};
|
||||
|
||||
const declarationChanged = declarationFields.some((field) => (
|
||||
patch[field] !== undefined && patch[field] !== current[field]
|
||||
));
|
||||
const repairsMissingStatus = current.birth_time_status === null
|
||||
&& current.active_birth_time === null
|
||||
&& current.birth_time === null
|
||||
&& current.rectification_case_id === null
|
||||
&& Boolean(patch.birth_time_source);
|
||||
if (!declarationChanged && !repairsMissingStatus) return {};
|
||||
return {
|
||||
active_birth_time: null,
|
||||
birth_time: null,
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
begin;
|
||||
|
||||
-- Repair declarations saved while the account route failed to derive the
|
||||
-- server-owned status. Applied/candidate/confirmed minutes remain untouched.
|
||||
update public.profiles
|
||||
set birth_time_status = 'reported',
|
||||
updated_at = pg_catalog.now()
|
||||
where birth_time_status is null
|
||||
and active_birth_time is null
|
||||
and birth_time is null
|
||||
and rectification_case_id is null
|
||||
and birth_date is not null
|
||||
and (
|
||||
(
|
||||
birth_time_source = 'hospital_record'
|
||||
and reported_birth_time is not null
|
||||
and birth_time_period is null
|
||||
and uncertainty_before_minutes = 2
|
||||
and uncertainty_after_minutes = 2
|
||||
)
|
||||
or (
|
||||
birth_time_source = 'family_exact'
|
||||
and reported_birth_time is not null
|
||||
and birth_time_period is null
|
||||
and uncertainty_before_minutes in (5, 10, 15)
|
||||
and uncertainty_after_minutes = uncertainty_before_minutes
|
||||
)
|
||||
or (
|
||||
birth_time_source = 'approximate'
|
||||
and reported_birth_time is not null
|
||||
and birth_time_period is null
|
||||
and uncertainty_before_minutes in (15, 30, 60)
|
||||
and uncertainty_after_minutes = uncertainty_before_minutes
|
||||
)
|
||||
or (
|
||||
birth_time_source = 'period_only'
|
||||
and reported_birth_time is null
|
||||
and birth_time_period in ('early_morning', 'morning', 'afternoon', 'evening', 'late_night')
|
||||
and uncertainty_before_minutes is null
|
||||
and uncertainty_after_minutes is null
|
||||
)
|
||||
or (
|
||||
birth_time_source = 'unknown'
|
||||
and reported_birth_time is null
|
||||
and birth_time_period is null
|
||||
and uncertainty_before_minutes is null
|
||||
and uncertainty_after_minutes is null
|
||||
)
|
||||
or (
|
||||
birth_time_source = 'legacy_import'
|
||||
and reported_birth_time is not null
|
||||
and birth_time_period is null
|
||||
and uncertainty_before_minutes is null
|
||||
and uncertainty_after_minutes is null
|
||||
)
|
||||
);
|
||||
|
||||
commit;
|
||||
@@ -12,6 +12,14 @@ import {
|
||||
const source = readFileSync(new URL("../src/app/api/account/route.ts", import.meta.url), "utf8");
|
||||
const patchSource = readFileSync(new URL("../src/lib/account-profile-patch.ts", import.meta.url), "utf8");
|
||||
const caseServiceSource = readFileSync(new URL("../src/lib/account-rectification-case.ts", import.meta.url), "utf8");
|
||||
const reportedStatusMigration = readFileSync(
|
||||
new URL("../supabase/migrations/20260726010000_backfill_reported_birth_time_status.sql", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
const productionMigrationWorkflow = readFileSync(
|
||||
new URL("../../.github/workflows/apply-production-rectification-migrations.yml", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
test("account API reads and returns the server-configured rectification price", () => {
|
||||
assert.match(source, /parseRectificationPriceCredits\(\s*process\.env\.RECTIFICATION_PRICE_CREDITS,?\s*\)/);
|
||||
@@ -435,6 +443,47 @@ test("ordinary declaration edits clear stale candidate application but never ove
|
||||
birth_time_status: "reported",
|
||||
rectification_case_id: null,
|
||||
});
|
||||
assert.deepEqual(resolveAccountBirthTimeApplicationPatch({
|
||||
...candidate,
|
||||
active_birth_time: null,
|
||||
birth_time: null,
|
||||
birth_time_status: null,
|
||||
rectification_case_id: null,
|
||||
reported_birth_time: edited.reported_birth_time,
|
||||
}, edited), {
|
||||
active_birth_time: null,
|
||||
birth_time: null,
|
||||
birth_time_status: "reported",
|
||||
rectification_case_id: null,
|
||||
});
|
||||
assert.deepEqual(resolveAccountBirthTimeApplicationPatch(null, edited), {
|
||||
active_birth_time: null,
|
||||
birth_time: null,
|
||||
birth_time_status: "reported",
|
||||
rectification_case_id: null,
|
||||
});
|
||||
assert.deepEqual(resolveAccountBirthTimeApplicationPatch({
|
||||
...candidate,
|
||||
birth_time_status: "confirmed",
|
||||
}, edited), {});
|
||||
assert.deepEqual(resolveAccountBirthTimeApplicationPatch({
|
||||
...candidate,
|
||||
active_birth_time: null,
|
||||
birth_time: null,
|
||||
birth_time_status: null,
|
||||
reported_birth_time: edited.reported_birth_time,
|
||||
}, edited), {});
|
||||
});
|
||||
|
||||
test("reported birth-time status repair is forward-only and wired into production migration flow", () => {
|
||||
assert.match(reportedStatusMigration, /birth_time_status is null/);
|
||||
assert.match(reportedStatusMigration, /birth_time_status = 'reported'/);
|
||||
assert.match(reportedStatusMigration, /active_birth_time is null/);
|
||||
assert.match(reportedStatusMigration, /rectification_case_id is null/);
|
||||
assert.equal(
|
||||
productionMigrationWorkflow.match(/20260726010000_backfill_reported_birth_time_status\.sql/g)?.length,
|
||||
2,
|
||||
);
|
||||
});
|
||||
|
||||
test("account PATCH uses the shared validator and never writes client birth_time over account truth", () => {
|
||||
|
||||
Reference in New Issue
Block a user