fix(onboarding): restore uncertain birth-time intake
This commit is contained in:
+19
-4
@@ -3310,11 +3310,26 @@
|
||||
- 首次发现:2026-08-15
|
||||
- 最近更新:2026-08-15
|
||||
- 影响面:初始化出生时间采集、`homepage/new` 生时校正 Case 基线与候选搜索范围、历史 Session 恢复。
|
||||
- 用户现象:用户再次新建校正时,系统可能以上一次采用的分钟而不是最初填报时间为中心,并继续继承旧的前后误差;初始化页面还要求用户选择误差分钟或大致时段。
|
||||
- 用户现象:用户再次新建校正时,系统可能以上一次采用的分钟而不是最初填报时间为中心,并继续继承旧的前后误差;只有大致时段或完全未知声明的 Profile 也可能被错误当成可创建精确分钟扫描的基线。
|
||||
- 触发条件:Profile 同时存在 `reported_birth_time`、历史 `active_birth_time` 和 uncertainty,或只有 period/unknown 声明时创建 fresh Case。
|
||||
- 根因:fresh Case 的范围推导优先使用 `active_birth_time`,再直接读取 Profile uncertainty;初始化模型把用户声明误差和引擎搜索窗口混为同一字段,并允许用 period 或全天范围代替具体初始时间。
|
||||
- 修复:初始化 UI 只采集一个具体 `reported_birth_time`,不再提供误差分钟、大致时段、范围线索或跳过入口;新填报的准确时间保存为 `reported + 0/0`,不自动宣称引擎 confirmed。`homepage/new` 只查询和使用 `reported_birth_time`,忽略历史 active minute、uncertainty 与 period;没有合法 reported time 时在调用创建 RPC 前以 `profile_incomplete` fail closed。Case 扫描所需的可移动窗口改为独立的服务器执行策略,目前以填报时间为中心使用前后 15 分钟,不再伪装成用户声明;`intent=session` 继续恢复历史 Case 自身冻结的 baseline/range。
|
||||
- 验证:回归覆盖新初始化只展示单一时间输入、`0/0` 持久化但不确认、旧 uncertainty 不影响 fresh range、旧 active minute 不进入 baseline、period/unknown/无具体时间 legacy profile 不得新建、失败前不调用 RPC,以及 session 恢复不读取当前 Profile;与 Focus、receipt 和 migration 回归合并运行 203 passed、0 failed。正式 staging 质量门禁另暴露两个旧测试合同,现已改为锁定“初始化仅填写一个具体时间”和 receipt tools 按真实 terminal receipt 时间排序,不再要求旧的不确定时间入口或字母排序。
|
||||
- 修复:新填报的准确时间保存为 `reported + 0/0`,不自动宣称引擎 confirmed;初始化仍允许用户如实声明大致时段或完全未知,但这些声明不能作为 fresh 精确分钟扫描的基线。`homepage/new` 只查询和使用合法 `reported_birth_time`,忽略历史 active minute、uncertainty 与 period;没有合法 reported time 时在调用创建 RPC 前以 `profile_incomplete` fail closed。Case 扫描所需的可移动窗口改为独立的服务器执行策略,目前以填报时间为中心使用前后 15 分钟,不再伪装成用户声明;`intent=session` 继续恢复历史 Case 自身冻结的 baseline/range。
|
||||
- 验证:回归覆盖准确时间 `0/0` 持久化但不确认、旧 uncertainty 不影响 fresh range、旧 active minute 不进入 baseline、period/unknown/无具体时间 legacy profile 不得新建、失败前不调用 RPC,以及 session 恢复不读取当前 Profile;初始化入口回归另由 BUG-197 锁定。与 Focus、receipt 和 migration 回归合并运行 203 passed、0 failed。
|
||||
- 防复发:`reported_birth_time` 是 fresh Case 唯一用户时间基线;`active_birth_time` 只表示已采用的当前排盘时间,不能反向改写新校正起点;用户声明字段、服务器搜索策略与最终 confirmed truth 必须保持分层。
|
||||
- 相关记录:BUG-127、BUG-177、BUG-187
|
||||
- 相关记录:BUG-127、BUG-177、BUG-187、BUG-197
|
||||
- 修复版本:本次功能分支提交(精确 SHA 以提交、远程分支与 staging 发布结果为准)
|
||||
|
||||
## BUG-197 | 精确分钟校正前置条件误删不确定和未知出生时间入口
|
||||
|
||||
- 状态:resolved
|
||||
- 首次发现:2026-08-15
|
||||
- 最近更新:2026-08-15
|
||||
- 影响面:初始化“出生日期与时间”表单、移动端资料填写、无准确出生时间用户的普通产品入口。
|
||||
- 用户现象:表单只显示“我知道准确出生时间”,原有“我不确定准确时间”、大致时段、补充描述和“完全不清楚,跳过出生时间”全部消失,无法准确填写分钟的用户无法继续。
|
||||
- 触发条件:staging 包含 `075c62e5` 后打开未确认出生时间的初始化表单。
|
||||
- 根因:BUG-196 修复把“fresh 生时校正 Case 必须有合法 `reported_birth_time`”错误扩大为“初始化表单只能接受具体时间”,同时测试也被改成明确禁止不确定/未知入口,导致业务回归被质量门禁当成正确结果。
|
||||
- 修复:恢复“我知道准确出生时间 / 我不确定准确时间”两条一级选择;不确定路径恢复大致时段、可选描述和完全未知跳过入口,未知状态允许返回描述范围。保留 BUG-196 的服务端边界:准确时间继续保存为 `reported + 0/0` 且不自动 confirmed;period/unknown 只能完成资料声明和使用无需分钟的功能,不能创建 fresh 精确分钟扫描。
|
||||
- 验证:先把两个错误测试合同改回用户路径合同并确认旧实现稳定失败,再恢复实现后通过;目标回归锁定 `family_exact + period_only` 两个一级选项、period/unknown 的真实组件分支、跳过入口、`reported` 状态及 consultation 的 minute-free 行为。390×844 Chrome 真实点击验证两个一级选项、时段表单、完全未知跳过和返回范围按钮均可见;滚动容器 `overflow-y: auto`,可从 `scrollTop=414` 滚到 `718` 并到达底部,`继续`按钮可进入视口。预览模式的三个 401 来自无登录态的只读背景接口,不影响本表单交互;仍需完成 staging 远端 SHA 验收。
|
||||
- 防复发:资料声明完整性和精确分钟校正可启动性必须是两个独立条件;任何 fresh Case 前置条件调整不得删除 period/unknown 资料入口。UI 合同测试必须正向断言两个一级选择、时段选择和跳过路径存在,禁止再用负向断言把产品能力删除写成门禁。
|
||||
- 相关记录:BUG-127、BUG-196
|
||||
- 修复版本:本次 staging 修复提交(精确 SHA 以远端分支核验结果为准)
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
|
||||
import { birthTimeConsultationOptionsCopy } from "@/lib/birth-time-consultation-consent";
|
||||
import {
|
||||
birthTimeDisplayState,
|
||||
birthTimePeriodOptions,
|
||||
birthTimeSourceDefaults,
|
||||
birthTimeSourceOptions,
|
||||
type BirthTimeDraft,
|
||||
@@ -70,6 +71,9 @@ export function BirthTimeIntakeFields({ value, onPatch }: BirthTimeIntakeProps)
|
||||
const source = value.birthTimeSource;
|
||||
const isConfirmed = value.birthTimeStatus === "confirmed";
|
||||
const displayState = birthTimeDisplayState(value);
|
||||
const knowledgeMode = source === "period_only" || source === "unknown"
|
||||
? "uncertain"
|
||||
: source ? "exact" : "";
|
||||
const usesClockTime = source === "hospital_record"
|
||||
|| source === "family_exact"
|
||||
|| source === "approximate"
|
||||
@@ -114,16 +118,20 @@ export function BirthTimeIntakeFields({ value, onPatch }: BirthTimeIntakeProps)
|
||||
)}
|
||||
|
||||
{!isConfirmed && <fieldset className="birth-time-source-fieldset">
|
||||
<legend>填写出生时间</legend>
|
||||
<p className="birth-time-source-intro">请填写你初始化提供的具体出生时间。</p>
|
||||
<legend>你对出生时间了解多少?</legend>
|
||||
<p className="birth-time-source-intro">不用猜具体分钟,先选一个最符合你的情况的选项。</p>
|
||||
<div className="birth-time-source-list">
|
||||
{birthTimeSourceOptions.map((option) => (
|
||||
<label
|
||||
className={`birth-time-source-option ${source === option.value ? "is-selected" : ""}`}
|
||||
className={`birth-time-source-option ${option.value === "family_exact"
|
||||
? knowledgeMode === "exact" ? "is-selected" : ""
|
||||
: knowledgeMode === "uncertain" ? "is-selected" : ""}`}
|
||||
key={option.value}
|
||||
>
|
||||
<input
|
||||
checked={source === option.value}
|
||||
checked={option.value === "family_exact"
|
||||
? knowledgeMode === "exact"
|
||||
: knowledgeMode === "uncertain"}
|
||||
name={`birth-time-source-${groupId}`}
|
||||
type="radio"
|
||||
value={option.value}
|
||||
@@ -158,6 +166,67 @@ export function BirthTimeIntakeFields({ value, onPatch }: BirthTimeIntakeProps)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{source === "period_only" && (
|
||||
<div className="birth-time-detail-grid birth-time-period-details onboarding-card-reveal">
|
||||
<label>
|
||||
<span>请选择最接近的时间范围</span>
|
||||
<Select
|
||||
required
|
||||
value={value.birthTimePeriod || null}
|
||||
onValueChange={(nextValue) => {
|
||||
if (typeof nextValue === "string") onPatch({ birthTimePeriod: nextValue });
|
||||
}}
|
||||
>
|
||||
<SelectTrigger aria-label="最接近的时间范围">
|
||||
<SelectValue placeholder="请选择大致时段">
|
||||
{(selectedValue) => birthTimePeriodOptions.find((option) => option.value === selectedValue)?.label ?? "请选择大致时段"}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{birthTimePeriodOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>{option.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</label>
|
||||
<label>
|
||||
<span>补充描述(可选)</span>
|
||||
<textarea
|
||||
maxLength={240}
|
||||
placeholder="例如:天刚亮、午饭前后、家人记得大约 6—8 点"
|
||||
rows={2}
|
||||
value={value.birthTimeClue}
|
||||
onChange={(event) => onPatch({ birthTimeClue: event.target.value })}
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
className="button-secondary birth-time-skip-button"
|
||||
type="button"
|
||||
onClick={() => onPatch({
|
||||
birthTimeSource: "unknown",
|
||||
reportedTime: "",
|
||||
birthTimePeriod: "",
|
||||
birthTimeClue: "",
|
||||
uncertaintyBeforeMinutes: null,
|
||||
uncertaintyAfterMinutes: null,
|
||||
birthTimeStatus: "reported",
|
||||
time: "",
|
||||
})}
|
||||
>完全不清楚,跳过出生时间</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{source === "unknown" && (
|
||||
<div className="birth-time-detail-note onboarding-card-reveal" role="status">
|
||||
<p>已跳过具体出生时间。保存后可以直接使用首页功能,生时校正以后需要时再做。</p>
|
||||
<button
|
||||
className="button-secondary"
|
||||
type="button"
|
||||
onClick={() => onPatch({ birthTimeSource: "period_only", birthTimeStatus: "reported" })}
|
||||
>我可以描述一个时间范围</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -84,6 +84,7 @@ export function formatBirthDate(value: Date): string {
|
||||
|
||||
export const birthTimeSourceOptions = [
|
||||
{ value: "family_exact", label: "我知道准确出生时间", hint: "保存为初始化填报时间,不会自动标记为引擎确认" },
|
||||
{ value: "period_only", label: "我不确定准确时间", hint: "告诉我们大致时段;完全不清楚也可以直接跳过" },
|
||||
] as const;
|
||||
|
||||
export const birthTimeSourceDefaults = {
|
||||
|
||||
@@ -206,18 +206,19 @@ test("unverified birth time no longer emits a modal or toast gate", () => {
|
||||
assert.doesNotMatch(page, /role="alertdialog"[\s\S]{0,300}出生时间还没有完成校正/);
|
||||
});
|
||||
|
||||
test("birth time intake only accepts one concrete initialization time", () => {
|
||||
test("birth time intake starts with exact or uncertain choices and keeps rectification optional", () => {
|
||||
const intake = readFileSync(new URL("../src/components/birth-time-intake.tsx", import.meta.url), "utf8");
|
||||
const model = readFileSync(new URL("../src/lib/birth-time-intake-model.ts", import.meta.url), "utf8");
|
||||
|
||||
assert.match(model, /我知道准确出生时间/);
|
||||
assert.match(intake, /请填写你初始化提供的具体出生时间/);
|
||||
assert.match(model, /我不确定准确时间/);
|
||||
assert.match(intake, /你对出生时间了解多少?/);
|
||||
assert.match(intake, /不用猜具体分钟/);
|
||||
assert.match(intake, /aria-label="选择出生时间"/);
|
||||
assert.doesNotMatch(model, /我不确定准确时间/);
|
||||
assert.doesNotMatch(intake, /source === "family_exact" \|\| source === "approximate"/);
|
||||
assert.doesNotMatch(intake, /请选择最接近的时间范围/);
|
||||
assert.doesNotMatch(intake, /完全不清楚,跳过出生时间/);
|
||||
assert.doesNotMatch(intake, /生时校正以后需要时再做/);
|
||||
assert.match(intake, /请选择最接近的时间范围/);
|
||||
assert.match(intake, /完全不清楚,跳过出生时间/);
|
||||
assert.match(intake, /生时校正以后需要时再做/);
|
||||
});
|
||||
|
||||
test("homepage and profile result copy use the source-aware consultation options", () => {
|
||||
|
||||
@@ -308,13 +308,19 @@ test("persisted birth dates normalize database ISO values without accepting inva
|
||||
assert.equal(normalizePersistedBirthDate("1997-08-08junk"), "");
|
||||
});
|
||||
|
||||
test("fresh intake only offers a concrete reported time and does not auto-confirm it", () => {
|
||||
test("fresh intake preserves exact, approximate-period, and unknown-time paths without auto-confirming", () => {
|
||||
const source = readFileSync(new URL("../src/components/birth-time-intake.tsx", import.meta.url), "utf8");
|
||||
|
||||
assert.deepEqual(birthTimeSourceOptions.map((option) => option.value), ["family_exact"]);
|
||||
assert.deepEqual(
|
||||
birthTimeSourceOptions.map((option) => option.value),
|
||||
["family_exact", "period_only"],
|
||||
);
|
||||
assert.doesNotMatch(source, /已用于当前排盘/);
|
||||
assert.doesNotMatch(source, /可能误差|选择误差范围|最接近的时间范围|大致时段|描述一个时间范围/);
|
||||
assert.doesNotMatch(source, /birthTimePeriodOptions|birthTimeSource: "period_only"|birthTimeSource: "unknown"/);
|
||||
assert.match(source, /birthTimePeriodOptions/);
|
||||
assert.match(source, /birthTimeSource: "unknown"/);
|
||||
assert.match(source, /请选择最接近的时间范围/);
|
||||
assert.match(source, /完全不清楚,跳过出生时间/);
|
||||
assert.match(source, /我可以描述一个时间范围/);
|
||||
assert.match(source, /birthTimeStatus: "reported"/);
|
||||
assert.match(source, /birthTimeConsultationOptionsCopy\(value\)/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user