diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fd45fce..b365e0da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ - 判断依据与证据比对时,宫位写法、中英文星体和星座名视为同一事实;修订时告诉写作具体哪个词没对上;合盘生成失败页标题改为「婚恋合盘」。占星 Skill 版本不变(BUG-1897)。 +## 2026-11-15 — 生时纠正暂时停用问卷和选边 + +- 直接显示填写时间或范围中点,保留六盘变化、快速选范围和历史记录。占星 Skill 版本不变(BUG-1896)。 + ## 2026-11-15 — 合盘分开显示正式核对和研究扫描 - 报告和追问分别展示五类事件的正式核对、研究日期和冲突说明。附录列出两人的运限条件、相关点位及法律婚姻缺项。旧报告重新打开时使用已保存的依据。占星 Skill 版本不变(BUG-1895)。 diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 2c1297ab..f296524b 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -25615,3 +25615,14 @@ - 验证与防复发:`tests/test_synastry_gate_repair_20261115.py`(混排证据通过、错宫错星仍拒、中英互认、D1、星宿、数组仍拒);`frontend/tests/synastry-gate-repair-20261115.test.ts`(修订细节、截断上限、改法说明、提示词、失败页标题)。 - 关联记录:#281、#289、BUG-1892、BUG-1895。 - 修复版本:`claude/synastry-gate-repair-20261115`。 + +## BUG-1896 | 生时纠正答完问卷仍回到填写时间或范围中点 + +- 状态:fixed-pending-verify(待独立验收)。 +- 首次发现 / 最近更新:2026-11-15 / 2026-11-15。 +- 触发与现象:首次进入或重新纠正时出现年龄选项和年份步进。选项未覆盖用户经历,答完也不会改变采用的时间或范围。 +- 根因:在答题结果不得缩窄自报范围的规则下,入口仍沿用问卷和选边。回测显示按答题缩窄表现低于随机。 +- 修复:默认直接给出填写时间或原范围中点。保留六盘、快速范围选择及历史。共用开关暂停问卷、选边和续答请求。 +- 验证与防复发:原生引擎夹具覆盖决定窗口、无分界线、历史重新进入、快速范围写回和中点。API 回归核对禁用请求的错误码。详见 [#292 进度](tasks/PROGRESS-rect-pause-20261115.md)。 +- 关联记录:#260、#279、BUG-1649、BUG-1886、BUG-1890。 +- 修复版本:`codex/rect-pause-20261115`,待独立验收。 diff --git a/docs/tasks/PROGRESS-rect-pause-20261115.md b/docs/tasks/PROGRESS-rect-pause-20261115.md new file mode 100644 index 00000000..890a37be --- /dev/null +++ b/docs/tasks/PROGRESS-rect-pause-20261115.md @@ -0,0 +1,31 @@ +# 生时纠正停用问卷和选边执行记录(2026-11-15) + +- 工单 #292,BUG-1896,范围按 Claude 的任务书。基线为 `1c9017698cc9fd577aa87b4615fc13f067126abe`,任务书提交为 `655e07ad2c76f077305f582ab20c350b812b85cd`。 + +## 开工盘点 + +| 步骤、组件 | API 和既有参数 | 原入口 | +| --- | --- | --- | +| `chart_range → chart_question → final`,`QuestionnaireRange`、`ChartQuestion`、`FinalStep` | `/api/chart-choice/packet` 的 `adaptive`、`window`。采用端点用 `adaptive`、`chart_answers`、`stop`、`mode` | 首次进入、首页重新纠正、改变范围 | +| `range → choose`,`RangeStep`、`RoundStep` | 采用端点用 `resolved`、`answers`、`event_answers`、`appearance`、`birthmarks`、`stop`。回到计算端点用非空 `resolved` 继续 | 旧 v2 结果和逐轮确认,返回一步继续原轮 | +| `choose → result`,`ChooseStep`、`ResultStep` | 采用端点用 `mode=preview/choice`、`answers`、`overrideSide`、`readBothAll` | 旧 v1 选盘,结果页可改选另一侧 | +| 续答兼容入口 | 两个端点的 `resumeRecordId`、`questionnaire` | 旧记录曾可继续确认。当前首页只有重新纠正,接口仍接收续答 | +| `narrow`,`ChartChoiceFlow` 中的缩小范围选项 | 计算端点用 `window` 提交所选子段 | 输入范围超出单次扫描上限 | +| 占星师问卷,`AstrologerStage` | `/api/chart-choice/astrologer` 的 `window` 和 `input`,含 `question_answers`、日期事件、胎记、外表、D9、D10 的选择 | 组件和端点保留,当前页面没有挂载该阶段 | + +历史首页以 `GET /api/chart-choice` 读取当前记录与停用记录。`restore_default`、`certain`、快速范围选择与历史读取按原有语义保留。 + +## 实现与截图 + +- 计算入口直接取原生引擎的默认结果。较宽范围沿用恢复默认值时的有界扫描,六盘变化和保存采用整段自报范围。用户原先没填范围时,保留 ±15 分钟试算,明确选择的范围在保存时写回。 +- 计算和采用端点对旧答题、选边和续答请求返回 HTTP 400 `questionnaire_paused`。停用的占星师问卷端点共用此错误码。页面和端点由同一个开关控制。 +- [四张 390 × 844 截图](https://git.copse.top/attachments/4b93d08e-d206-477a-8e97-c86f7e40d528) 分别为填写时间和范围、只填时间、只填范围、已有记录。Chrome 使用实际组件、生产构建 CSS 和原生引擎的虚构样例响应。截图环境用本地夹具替代登录后的 API。附件 SHA-256 为 `1fa22b021d2974d40c27e8505c2656aaeb76deca470e7cf17e89884198c9d5a7`。 + +## 验证 + +- 停用开关时的生时纠正定向测试 33 项通过。启用开关的隔离 worktree 中,33 项原有生时纠正和记录测试通过。既有断言未修改。 +- `tsc --noEmit` 通过,前端 lint 0 错误。生产构建通过,`/` 和 `/rectification` 都保持静态。开工基线和候选的首页 39 个入口 JS/CSS 资产 gzip 合计均为 614 357 字节,变化 0%。 +- Windows 前端全量,开工基线 3 782 项、通过 3 699 项,候选 3 785 项、通过 3 702 项。两边均有 46 项失败(含 Bash、部署和数据库环境限制),失败用例逐项相同,37 项跳过,0 项取消。 +- 仓库隐私测试 62 项通过。 +- Windows Python 快速门执行到 pytest,1 497 项通过、2 项跳过、29 个子测试通过,3 项失败。对开工基线单独运行 `tests/test_upstream_technique_layers.py` 复现同三项失败。触发点为 Kaala 的 1955 年负 Unix 时间戳经 Windows `datetime.fromtimestamp` 转换时报 `OSError 22`,另两项缺少 `seed`。预检确认远端、运行时和适配器可用,碎片审计和预检的定向检查分别触及 90 秒和 45 秒时限。 +- staging 新增 `12684c8b8` 后,在隔离工作树按该提交与本单前端变更的组合复测。生时纠正与合盘修订定向用例合计前端 37 项、Python 56 项通过。组合生产构建保持 `/` 与 `/rectification` 静态,首页入口资产 gzip 仍为 614 357 字节。其后 `3b510608e` 仅调整个人报告测试,该文件 8 项通过。 diff --git a/docs/tasks/TASK-rect-pause-20261115.md b/docs/tasks/TASK-rect-pause-20261115.md new file mode 100644 index 00000000..c46dcb34 --- /dev/null +++ b/docs/tasks/TASK-rect-pause-20261115.md @@ -0,0 +1,87 @@ +# 生时纠正:先停用问卷和选边(2026-11-15,Claude) + +Bug 编号:BUG-1896。 + +## 背景 + +产品负责人 11-15 在 staging 真机上走完一遍问卷(#279 上线后),反馈: + +- **选项残缺:** 选项只有能区分候选段的年龄(例如事业盘只有「26 岁」),用户 30 岁创业就找不到对应选项;年份只能在该年龄覆盖的一两年里调。 +- **选项样式难看。** +- **答完没有结果:** 结论永远是「按你填的 05:00」和「现有信息不足以确认更准确的出生时间」。 + +最后一条是根本问题。#260 的回测显示,按答题缩窄比随机还差;占星师 11-14 裁定不得缩窄,#279 把旧窄范围也撤回了。所以现在答题不可能改变结果,问卷只是让用户白答。 + +负责人 11-15 决定**先停用问卷**。等占星师下一轮(日期区间计分、新留出集)验证有效后再开,届时重做选项 UI。 + +## 改法 + +### 一、盘点(先做,写进 PROGRESS) + +列出生时纠正里所有会向用户提问、或让用户选边、选段的入口,每条写清: + +- step 名、组件名、对应 API 及参数; +- 现在从哪里进入(首页「重新纠正」、首次进入、换范围后等)。 + +已知的入口: + +- 自适应问卷:`chart_range` → `chart_question` → `final`; +- v2 轮次:`choose` 里的 `RoundStep`; +- v1:`ChooseStep` → `ResultStep`,含「换另一侧」; +- `astrologer-stage.tsx`:「感情里你更像哪一种?」等。 + +盘点中发现的其他入口同样处理。 + +### 二、页面(只改 `frontend/src/components/chart-choice/` 和对应测试) + +首次进入和「重新纠正」都直接显示结果页,不出任何题目。结果页即现在问卷答完后的结果页(真机截图第 4 张)去掉「你提到的事」,内容依次为: + +1. 标题「按你填的 05:00」(没填时间:「按范围中点 HH:MM」)。 +2. 「范围 04:45–05:15」。没填范围时保留现有的范围选择(`RangeChips`)。 +3. 「现有信息不足以确认更准确的出生时间。」 +4. 范围条和参照时间。 +5. 「按 05:00 看六张盘」六行,文案沿用 #279 的规则: + - 默认窗口:「前后 15 分钟试算里没有变化」; + - 用户填了范围:「在你填的范围里没有换座」; + - 会换座:「范围内会换座(hh:mm:ss 换)」。 +6. 唯一的主按钮「用 05:00」,保存 `reported_time_default`(没填时间时为 `window_midpoint_default`)。保存后回首页。 + +如果范围内没有分界线,`certain` 分支照旧。 + +还要去掉: + +- 「开始答题」「先到这里」「下一题」「都没有」「记不清」; +- 年龄选项、哪一年步进器; +- 「换另一侧」「更像哪一种」; +- 「你提到的事」。 + +问卷相关组件和代码**保留不删**,只是不再渲染。用一个常量开关(例如 `RECT_QUESTIONNAIRE_ENABLED = false`)集中控制,方便以后重开。 + +### 三、API + +- `/api/chart-choice/adopt` 和 `/api/chart-choice/packet` 收到问卷或选边类请求时,返回 4xx 并带明确说明(code 例如 `questionnaire_paused`)。问卷或选边类请求包括 `chart_answers`、`questionnaire`、`answers`、`overrideSide`、`resolved` 非空、`resumeRecordId` 续答等,以盘点为准。 +- `adaptive` 只取默认结果(不出题),保存仍走现有 `reported_time_default` / `window_midpoint_default`。 +- `restore_default`、`certain`、范围选择保持可用。 +- 不改迁移、不改引擎评分、不删旧枚举。 + +### 四、历史和下游 + +- 已有记录照旧显示,#279 的「已停用」逻辑不动。 +- 已有的明确手动选边、#229 选边记录继续按 #279 的规则读取,本单不改。 +- 报告、年运、聊天、合盘不受影响,不碰 `scripts/report_render/`。 + +## 验收 + +- **测试:** + - 首次进入与「重新纠正」直接渲染结果页,没有题目、没有「先到这里」「下一题」「更像」「换另一侧」「你提到的事」; + - 唯一按钮「用 HH:MM」,保存 `reported_time_default`;没填时间的保存 `window_midpoint_default`; + - 没填范围时可选范围,保存后范围写回(BUG-1649 行为不变); + - API 拒绝问卷和选边请求,返回 4xx 并带 code; + - 开关打开时现有问卷测试仍通过(证明代码保留可用)。 +- **截图(390 宽):** 填了时间和范围;只填时间(默认 ±15);没填时间;首页已有记录。 +- **门禁:** Python quick gate、chart-choice 相关前端测试、`tsc`、`npx eslint .` 0 errors。 +- **文档:** + - `docs/BUG_HISTORY.md` 新增 BUG-1896(只追加这一节):现象、根因(问卷在不能缩窄的规则下没有产出)、修复、验证、防复发,关联 BUG-1886、BUG-1890; + - CHANGELOG 一行; + - 更新 `frontend/DESIGN.md` 和 `frontend/docs/VOICE.md` 里涉及问卷的段落; + - 写 PROGRESS。 diff --git a/frontend/DESIGN.md b/frontend/DESIGN.md index 7f982cfe..30dd76cc 100644 --- a/frontend/DESIGN.md +++ b/frontend/DESIGN.md @@ -358,10 +358,12 @@ The self-profile editor in 星盘资料 (`ChartLibraryPanel`) owns the ayanamsa ### Rectification surface -- **按盘选盘(#239,BUG-1873):** 范围、答题、结果和当前记录用平面布局,靠留白和细线分组。答题用 36px 时间带、22px 衬线标题和浅底选项,选项内的 8px 小带表示覆盖范围。事件选项原地展开年份和类别,展开 0.32 秒,遵循系统的减少动态效果设置。结果突出候选段,六盘用细带展示段内换座,并列出证据与冲突。首页保留星盘、重做和按未定盘续问。 -- **自适应生时纠正(#260,BUG-1881):** 题目随候选范围变化。细进度条对应已确认盘数,年龄旁附公历年份。选项以同色底、1.5px 内描边和强调色文字及细带表示选中。事件类别用细边框与实心选中态,年份加减器保持透明底,展开仍为 0.32 秒。深色模式的选项和细带使用反转表面 token。刻度标签接近时移到另一侧,腾出靠近的端点。当前记录用代表时刻作标题,紧随范围和六盘状态。 +- **问卷暂停(#292,BUG-1896)**首次进入和重新纠正直接打开原问卷的结果布局。先显示填写时间或范围中点,再显示范围和计算说明;没填范围时在范围下保留快速范围选项。原时间带、六盘列表和单个「用 HH:MM」主按钮沿用。历史首页保留「重新纠正」及停用记录。 + +- **按盘选盘(#239,BUG-1873,问卷暂停前):** 范围、答题、结果和当前记录用平面布局,靠留白和细线分组。答题用 36px 时间带、22px 衬线标题和浅底选项,选项内的 8px 小带表示覆盖范围。事件选项原地展开年份和类别,展开 0.32 秒,遵循系统的减少动态效果设置。结果突出候选段,六盘用细带展示段内换座,并列出证据与冲突。首页保留星盘、重做和按未定盘续问。 +- **自适应生时纠正(#260,BUG-1881,问卷暂停前):** 题目随候选范围变化。细进度条对应已确认盘数,年龄旁附公历年份。选项以同色底、1.5px 内描边和强调色文字及细带表示选中。事件类别用细边框与实心选中态,年份加减器保持透明底,展开仍为 0.32 秒。深色模式的选项和细带使用反转表面 token。刻度标签接近时移到另一侧,腾出靠近的端点。当前记录用代表时刻作标题,紧随范围和六盘状态。 - **生时纠正停用的结果(#279,BUG-1890):** 首页先显示填写时间或原范围中点,再显示原范围。已停用的记录在历史里可展开查看当时的时间和范围。没有填误差时,六盘行写试算窗口内的变化,默认前后各 15 分钟。填写过范围时,只描述该范围内是否换座。 -- **问卷结果(#260,BUG-1886):** 标题下依次显示原范围和「现有信息不足以确认更准确的出生时间。」时间带只画原范围和参照时刻,不画答题段;只有一个主按钮「用 HH:MM」,不提供收窄或比较(FIX-7)。事件列表只列用户提供的事件。比较态的首页显示「你选的比较范围」、原填写时间或范围,以及描边恢复按钮。 +- **问卷结果(#260,BUG-1886,问卷暂停前):** 标题下依次显示原范围和「现有信息不足以确认更准确的出生时间。」时间带只画原范围和参照时刻,不画答题段;只有一个主按钮「用 HH:MM」,不提供收窄或比较(FIX-7)。事件列表只列用户提供的事件。比较态的首页显示「你选的比较范围」、原填写时间或范围,以及描边恢复按钮。 The birth-time rectification session is the consultation transcript plus a house board; questions live inside the assistant message that asked them. It shares every waiting vocabulary with the consultation surface; nothing here spins or says "loading" after the reveal. diff --git a/frontend/docs/VOICE.md b/frontend/docs/VOICE.md index 2f5dea5e..e2b7a6e4 100644 --- a/frontend/docs/VOICE.md +++ b/frontend/docs/VOICE.md @@ -620,8 +620,8 @@ Jyotisha 的可见文案是产品的一部分。正确性红线(真实性、 星盘档案里问别人和你的关系,标题只用「TA 是你的」。选项只有:伴侣、孩子、父母、朋友、其他。不写「客户」「家人」来代替这五项。没选过的,名字后面的小标签写「其他」。本人的标签仍是「本人」,不问这一句。 -## 生时纠正问卷(#260) +## 生时纠正结果(#260、#292) -- 结果页在范围后写「现有信息不足以确认更准确的出生时间。」不提供收窄或比较按钮(FIX-7)。事件列表只写日期和类别。 +- 首次进入或点「重新纠正」后写「按你填的 HH:MM」,没填时间时写「按范围中点 HH:MM」。范围后写「现有信息不足以确认更准确的出生时间。」只有「用 HH:MM」一个主按钮。没填范围时给快速范围选项。 - 旧答题或事件推得的范围停用后,首页写「你填的时间」或「原范围中点」,历史记录写「已停用」。未填误差时,盘未换座写「前后 15 分钟试算里没有变化」。用户填过范围时写「在你填的范围里没有换座」。 diff --git a/frontend/src/app/api/chart-choice/adopt/route.ts b/frontend/src/app/api/chart-choice/adopt/route.ts index 4d64062a..b03332d6 100644 --- a/frontend/src/app/api/chart-choice/adopt/route.ts +++ b/frontend/src/app/api/chart-choice/adopt/route.ts @@ -1,8 +1,9 @@ import { NextResponse } from "next/server"; +import { isRectQuestionnaireRequest, RECT_QUESTIONNAIRE_ENABLED, RECT_QUESTIONNAIRE_PAUSED } from "@/components/chart-choice/questionnaire-switch"; import { z } from "zod"; import { chartAnswersSchema, chartChoiceCaller, chartChoiceErrorResponse, evidenceInput, resolvedSchema, windowSchema } from "@/lib/chart-choice/route-support"; -import { adoptCertainTime, adoptChartChoice, previewAdaptiveChartChoice, previewChartChoice, restoreChartChoiceDefault } from "@/lib/chart-choice/server"; +import { adoptCertainTime, adoptChartChoice, adoptDefaultChartChoice, previewAdaptiveChartChoice, previewChartChoice, readDefaultChartChoice, restoreChartChoiceDefault } from "@/lib/chart-choice/server"; import { createAdminSupabaseClient } from "@/lib/supabase/admin"; export const runtime = "nodejs"; @@ -44,9 +45,22 @@ export async function POST(request: Request) { code: "answer_window_disabled", }, { status: 400 }); } + if (!RECT_QUESTIONNAIRE_ENABLED && isRectQuestionnaireRequest(body)) { + return NextResponse.json(RECT_QUESTIONNAIRE_PAUSED, { status: 400 }); + } const parsed = bodySchema.safeParse(body); if (!parsed.success) return NextResponse.json({ error: "请求格式不对", code: "bad_request" }, { status: 400 }); try { + if (!RECT_QUESTIONNAIRE_ENABLED && (parsed.data.mode === "preview" || parsed.data.mode === "choice")) { + const { subjectId, window, rangeChosen } = parsed.data; + if (parsed.data.mode === "preview") { + const { decision } = await readDefaultChartChoice(caller.supabase, caller.userId, { subjectId, window }); + return NextResponse.json({ decision }); + } + return NextResponse.json(await adoptDefaultChartChoice( + caller.supabase, createAdminSupabaseClient(), caller.userId, { subjectId, window, rangeChosen }, + )); + } if (parsed.data.mode === "restore_default") { return NextResponse.json(await restoreChartChoiceDefault( caller.supabase, createAdminSupabaseClient(), caller.userId, parsed.data, diff --git a/frontend/src/app/api/chart-choice/astrologer/route.ts b/frontend/src/app/api/chart-choice/astrologer/route.ts index bfc997da..55003599 100644 --- a/frontend/src/app/api/chart-choice/astrologer/route.ts +++ b/frontend/src/app/api/chart-choice/astrologer/route.ts @@ -1,4 +1,5 @@ import { NextResponse } from "next/server"; +import { RECT_QUESTIONNAIRE_ENABLED, RECT_QUESTIONNAIRE_PAUSED } from "@/components/chart-choice/questionnaire-switch"; import { z } from "zod"; import { chartChoiceCaller, chartChoiceErrorResponse, windowSchema } from "@/lib/chart-choice/route-support"; @@ -28,6 +29,9 @@ const bodySchema = z.object({ export async function POST(request: Request) { const caller = await chartChoiceCaller("POST /api/chart-choice/astrologer"); if (!caller.ok) return caller.response; + if (!RECT_QUESTIONNAIRE_ENABLED) { + return NextResponse.json(RECT_QUESTIONNAIRE_PAUSED, { status: 400 }); + } const parsed = bodySchema.safeParse(await request.json().catch(() => null)); if (!parsed.success) return NextResponse.json({ error: "请求格式不对", code: "bad_request" }, { status: 400 }); try { diff --git a/frontend/src/app/api/chart-choice/packet/route.ts b/frontend/src/app/api/chart-choice/packet/route.ts index 76a5eb67..c9d342b2 100644 --- a/frontend/src/app/api/chart-choice/packet/route.ts +++ b/frontend/src/app/api/chart-choice/packet/route.ts @@ -1,9 +1,10 @@ import { NextResponse } from "next/server"; +import { isRectQuestionnaireRequest, RECT_QUESTIONNAIRE_ENABLED, RECT_QUESTIONNAIRE_PAUSED } from "@/components/chart-choice/questionnaire-switch"; import { z } from "zod"; import { chartChoiceCaller, chartChoiceErrorResponse, resolvedSchema, windowSchema } from "@/lib/chart-choice/route-support"; import { createAdminSupabaseClient } from "@/lib/supabase/admin"; -import { buildChartChoicePacket } from "@/lib/chart-choice/server"; +import { buildChartChoicePacket, readDefaultChartChoice } from "@/lib/chart-choice/server"; export const runtime = "nodejs"; @@ -20,9 +21,16 @@ const bodySchema = z.object({ export async function POST(request: Request) { const caller = await chartChoiceCaller("POST /api/chart-choice/packet"); if (!caller.ok) return caller.response; - const parsed = bodySchema.safeParse(await request.json().catch(() => null)); + const body: unknown = await request.json().catch(() => null); + if (!RECT_QUESTIONNAIRE_ENABLED && isRectQuestionnaireRequest(body)) { + return NextResponse.json(RECT_QUESTIONNAIRE_PAUSED, { status: 400 }); + } + const parsed = bodySchema.safeParse(body); if (!parsed.success) return NextResponse.json({ error: "请求格式不对", code: "bad_request" }, { status: 400 }); try { + if (!RECT_QUESTIONNAIRE_ENABLED) { + return NextResponse.json(await readDefaultChartChoice(caller.supabase, caller.userId, parsed.data)); + } return NextResponse.json(await buildChartChoicePacket(caller.supabase, caller.userId, parsed.data, undefined, parsed.data.resumeRecordId ? createAdminSupabaseClient() : undefined)); } catch (error) { diff --git a/frontend/src/components/chart-choice/astrologer-stage.tsx b/frontend/src/components/chart-choice/astrologer-stage.tsx index 48fa01f0..526699c3 100644 --- a/frontend/src/components/chart-choice/astrologer-stage.tsx +++ b/frontend/src/components/chart-choice/astrologer-stage.tsx @@ -4,6 +4,7 @@ import { Fragment, useCallback, useEffect, useRef, useState, type ReactNode } fr import { ChevronLeft, Plus, X } from "lucide-react"; import { ChartDrawLoader } from "@/components/chart-draw-loader"; +import { RECT_QUESTIONNAIRE_ENABLED } from "@/components/chart-choice/questionnaire-switch"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; @@ -30,7 +31,11 @@ import { cn } from "@/lib/utils"; * 选择题、外表体质、胎记、带日期的大事(含六亲)、D9 / D10 特质,再按她的标准给吻合率和置信度。 * 选盘页不变;这里只并排给出她的方法的结果和我们的回测说明。 */ -export function AstrologerStage({ +export function AstrologerStage(props: Parameters[0]) { + return RECT_QUESTIONNAIRE_ENABLED ? : null; +} + +function AstrologerQuestionnaire({ subjectId, window, chartChoiceTime, diff --git a/frontend/src/components/chart-choice/chart-choice-page.tsx b/frontend/src/components/chart-choice/chart-choice-page.tsx index 4a189000..c4c85444 100644 --- a/frontend/src/components/chart-choice/chart-choice-page.tsx +++ b/frontend/src/components/chart-choice/chart-choice-page.tsx @@ -6,6 +6,7 @@ import { ChevronLeft, ChevronRight } from "lucide-react"; import { AppLink } from "@/components/app-link"; import { ChartDrawLoader } from "@/components/chart-draw-loader"; import { MiniVargaChart } from "@/components/chart-choice/mini-varga-chart"; +import { RECT_QUESTIONNAIRE_ENABLED } from "@/components/chart-choice/questionnaire-switch"; import { SecondaryPageShell } from "@/components/secondary-page-shell"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -159,6 +160,19 @@ export function ChartChoiceFlow({ subjectId }: { readonly subjectId: string }) { ...(request.resolved === undefined ? { adaptive: true } : {}), }); if (ticket !== generation.current) return; + if (!RECT_QUESTIONNAIRE_ENABLED) { + if (packet.status === "certain") { + setStep({ kind: "certain", packet }); + } else if (packet.status === "complete" && packet.decision.reported_choice) { + setStep({ kind: "final", packet, decision: packet.decision, + body: { subjectId, adaptive: true, mode: "choice", ...(request.window ? { window: request.window } : {}) }, + back: { kind: "home" }, + }); + } else { + throw new Error("暂时没有可用的结果,请重试。"); + } + return; + } if (packet.status === "needs_narrowing") setStep({ kind: "narrow", packet }); else if (packet.status === "questionnaire") { const range: Step = { kind: "chart_range", packet, request, previous }; @@ -448,7 +462,10 @@ export function ChartChoiceFlow({ subjectId }: { readonly subjectId: string }) { ); case "final": return restore(step.back)} onAccept={() => void saveFinal(step)} />; + defaultOnly={!RECT_QUESTIONNAIRE_ENABLED} + rangeChips={!RECT_QUESTIONNAIRE_ENABLED ? rangeChips(step.packet) : null} + onBack={RECT_QUESTIONNAIRE_ENABLED ? () => restore(step.back) : hasHistory ? toHome : undefined} + onAccept={() => void saveFinal(step)} />; } } } @@ -974,28 +991,31 @@ function matchedEvidence(decision: FinalDecision): string | null { return `${labels.join("、")},和这段时间的大运对得上${count < 3 ? ",先作参考" : ""}。`; } -export function FinalStep({ packet, decision, pending, onBack, onAccept, trial = false }: { +export function FinalStep({ packet, decision, pending, onBack, onAccept, trial = false, defaultOnly = false, rangeChips }: { readonly packet: FinalPacket; readonly decision: FinalDecision; readonly pending: boolean; - readonly onBack: () => void; + readonly onBack?: () => void; readonly onAccept: () => void; readonly trial?: boolean; + readonly defaultOnly?: boolean; + readonly rangeChips?: ReactNode; }) { const seconds = decision.saved_at.length === 19 && !decision.saved_at.endsWith(":00"); const saved = clockLabel(decision.saved_at, packet.reported_at, seconds); - if (packet.status === "questionnaire") { + if (defaultOnly || packet.status === "questionnaire") { const reported = "reported_choice" in decision ? decision.reported_choice : undefined; const reference = reported?.saved_at ?? packet.reported_at; const defaultWindow = reported?.segment ?? packet.window; const hasReportedTime = reported?.reported_known ?? packet.reported_known; const clock = clockLabel(reference); - const events = decision.events ?? []; + const events = defaultOnly ? [] : decision.events ?? []; return
- {!pending ? 改答案 : null} + {!pending && onBack ? {defaultOnly ? "返回" : "改答案"} : null}

{hasReportedTime ? `按你填的 ${clock}` : `按范围中点 ${clock}`}

范围 {segmentLabel(defaultWindow, reference)}

+ {rangeChips}

现有信息不足以确认更准确的出生时间。

@@ -1016,7 +1036,7 @@ export function FinalStep({ packet, decision, pending, onBack, onAccept, trial = const evidence = matchedEvidence(decision); return <> - {!pending ? 改答案 : null} + {!pending && onBack ? 改答案 : null}

确认了 {decision.rounds?.length ?? 0} 处

候选时间段

{segmentLabel(decision.segment)}

diff --git a/frontend/src/components/chart-choice/questionnaire-switch.ts b/frontend/src/components/chart-choice/questionnaire-switch.ts new file mode 100644 index 00000000..c5f7753a --- /dev/null +++ b/frontend/src/components/chart-choice/questionnaire-switch.ts @@ -0,0 +1,19 @@ +/** Product pause. Switch on to restore both the page and the questionnaire endpoints. */ +export const RECT_QUESTIONNAIRE_ENABLED = false; + +const QUESTIONNAIRE_FIELDS = [ + "questionnaire", "chart_answers", "answers", "overrideSide", "readBothAll", "resumeRecordId", + "event_answers", "appearance", "birthmarks", +] as const; + +export function isRectQuestionnaireRequest(body: unknown): boolean { + if (!body || typeof body !== "object" || Array.isArray(body)) return false; + const fields = body as Record; + return QUESTIONNAIRE_FIELDS.some((key) => Object.hasOwn(fields, key)) + || (Array.isArray(fields.resolved) && fields.resolved.length > 0); +} + +export const RECT_QUESTIONNAIRE_PAUSED = { + error: "答题和选边暂已停用。现在可以保存填写的时间或范围中点。", + code: "questionnaire_paused", +} as const; diff --git a/frontend/src/lib/chart-choice/server.ts b/frontend/src/lib/chart-choice/server.ts index 048a28cf..26b6631c 100644 --- a/frontend/src/lib/chart-choice/server.ts +++ b/frontend/src/lib/chart-choice/server.ts @@ -441,6 +441,8 @@ function terminalChoice({ result, subject }: Awaited MAX_RECORD_SPAN_MINUTES * 2; + const around = wide ? windowAround(reference, MAX_RECORD_SPAN_MINUTES) : declared; + const window = { + start_at: around.start_at > declared.start_at ? around.start_at : declared.start_at, + end_at: around.end_at < declared.end_at ? around.end_at : declared.end_at, + }; + const packet = await engine("/api/chart_choice/decide", { + ...engineBody(subject, window), adaptive: true, stop: true, + ...(wide ? { reported_at: reference, reported_default_window: declared } : {}), + }, fetchImpl, wide ? RESTORE_WIDE_RANGE_TIMEOUT_MS : ENGINE_TIMEOUT_MS); + if (packet.status !== "complete" || packet.engine_version !== ROUNDS_VERSION || !packet.decision.reported_choice) { + throw new ChartChoiceError("no_choice", 409); + } + const reported = { ...packet.decision.reported_choice, reported_known: at !== null }; + return { subject, packet, decision: { ...packet.decision, reported_choice: reported }, answers: {}, reported, wide }; +} + +type DefaultChoiceRequest = Pick; + +async function loadDefaultChartChoice(supabase: unknown, userId: string, request: DefaultChoiceRequest, fetchImpl?: typeof fetch) { + const subject = await loadSubject(supabase, userId, request.subjectId); + const requested = scanWindow(subject, request.window); + const window = subject.declared && declaredNeedsRange(subject.declared) ? requested : scanWindow(subject, undefined); + return defaultChartChoiceOutcome(subject, window, fetchImpl); +} + +export async function readDefaultChartChoice( + supabase: unknown, userId: string, request: DefaultChoiceRequest, fetchImpl?: typeof fetch, +) { + const result = await loadDefaultChartChoice(supabase, userId, request, fetchImpl); + const { packet, reported, decision, subject } = result; + if (!result.wide && !reported.remaining_key_lines && !packet.minor_lines?.length) { + // The existing certain flow checks legacy minor boundaries too. + const legacy = await engine("/api/chart_choice/packet", engineBody(subject, reported.segment), fetchImpl); + if (legacy.status === "certain") return { ...legacy, decision: publicDecision(decision) }; + } + return { + engine_version: packet.engine_version, status: "complete" as const, + window: reported.segment, reported_at: reported.saved_at, reported_known: reported.reported_known, + saved_at: reported.saved_at, decision: publicDecision(decision), + }; +} + +export async function adoptDefaultChartChoice( + supabase: unknown, admin: RpcClient, userId: string, request: DefaultChoiceRequest, fetchImpl?: typeof fetch, +) { + const choice = await loadDefaultChartChoice(supabase, userId, request, fetchImpl); + return saveChartChoice(admin, userId, { ...request, adaptive: true }, choice); +} + /** Restore the default for the current comparison using the declaration and no questionnaire answers. */ export async function restoreChartChoiceDefault( supabase: unknown, @@ -503,34 +559,12 @@ export async function restoreChartChoiceDefault( || current.detail.provenance?.source !== "user_choice_from_answers") { throw new ChartChoiceError("record_stale", 409); } - const declared = scanWindow(subject, undefined); - const at = subject.declared && reportedAt(subject.declared); - const start = minutesOf(declared.start_at); - const end = minutesOf(declared.end_at); - let window = declared; - let reference = at; - if (end - start > MAX_RECORD_SPAN_MINUTES * 2) { - // The engine accepts a short scan plus the full range for its default projection. - reference ??= midpointAt(declared); - const around = windowAround(reference, MAX_RECORD_SPAN_MINUTES); - window = { start_at: around.start_at > declared.start_at ? around.start_at : declared.start_at, - end_at: around.end_at < declared.end_at ? around.end_at : declared.end_at }; - } - const packet = await engine("/api/chart_choice/decide", { - ...engineBody(subject, window), adaptive: true, stop: true, - ...(window !== declared ? { reported_at: reference, reported_default_window: declared } : {}), - }, fetchImpl, window === declared ? ENGINE_TIMEOUT_MS : RESTORE_WIDE_RANGE_TIMEOUT_MS); - if (packet.status !== "complete" || packet.engine_version !== ROUNDS_VERSION || !packet.decision.reported_choice) { - throw new ChartChoiceError("no_choice", 409); - } + const choice = await defaultChartChoiceOutcome(subject, scanWindow(subject, undefined), fetchImpl); const latest = await readHistory(admin, userId, subject.subjectId); if (latest.current_record_id !== current.id || !latest.records.some((record) => record.id === current.id && record.reusable)) { throw new ChartChoiceError("record_stale", 409); } - const { decision } = await saveChartChoice(admin, userId, { adaptive: true }, { - packet, subject, answers: {}, - decision: { ...packet.decision, reported_choice: { ...packet.decision.reported_choice, reported_known: at !== null } }, - }); + const { decision } = await saveChartChoice(admin, userId, { adaptive: true }, choice); return { saved_at: decision.saved_at }; } diff --git a/frontend/tests/chart-choice-pause-20261115.test.tsx b/frontend/tests/chart-choice-pause-20261115.test.tsx new file mode 100644 index 00000000..4cc54a89 --- /dev/null +++ b/frontend/tests/chart-choice-pause-20261115.test.tsx @@ -0,0 +1,213 @@ +import "./install-test-document"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { register } from "node:module"; +import { spawnSync } from "node:child_process"; +import test from "node:test"; +import React from "react"; + +import { ChartChoiceFlow } from "../src/components/chart-choice/chart-choice-page"; +import type { ChartChoiceEnginePacket, ChartChoiceRecord, ChartChoiceStart } from "../src/lib/chart-choice/contract"; +import { createClientLifecycleHarness } from "./react-client-lifecycle-test-support"; + +Object.assign(globalThis, { React }); +register(new URL("./support/stub-server-only.mjs", import.meta.url).href, import.meta.url); + +type Complete = Extract; +type Certain = Extract; +type Ready = Extract; + +const fixtures = JSON.parse(readFileSync(new URL("./fixtures/chart-choice-pause-native-20261115.json", import.meta.url), "utf8")) as { + defaultTrial: Complete; declared: Complete; midpoint: Complete; wideMidpoint: Complete; + certain: { legacy: Certain; default: Complete }; +}; + +function startFor(packet: Complete, rangeAdjustable: boolean, record?: ChartChoiceRecord): Ready { + const defaultChoice = packet.decision.reported_choice!; + return { + status: "ready", subjectId: "self", name: "虚构人物", + window: defaultChoice.segment, rangeAdjustable, + reportedAt: defaultChoice.reported_known ? defaultChoice.saved_at : null, + precision: defaultChoice.reported_known ? "minutes" : "period", + history: { current_record_id: record?.id ?? null, records: record ? [record] : [] }, + }; +} + +function currentRecord(packet: Complete): ChartChoiceRecord { + const saved = packet.decision.reported_choice!; + return { + id: "test-current", created_at: "2026-11-15T00:00:00Z", saved_date: saved.saved_at.slice(0, 10), + saved_time: saved.saved_at.slice(11), chosen_side: "none", rule: "reported_time_default", reusable: true, + detail: { + window: saved.segment, reported_at: saved.saved_at, line: null, other_lines: saved.remaining_key_lines, + questions: [], answers: {}, unchanged: [], charts: [], + decision: { + ...packet.decision, ...saved, chosen_side: "none", votes: null, + both_versions_domains: [], rule: "reported_time_default", + }, + }, + }; +} + +test("first entry and restart open the default result, preserve trial language and save the chosen range", async () => { + for (const scenario of [ + { packet: fixtures.declared, adjustable: false, exported: "按你填的 05:00", restart: false }, + { packet: fixtures.defaultTrial, adjustable: true, exported: "按你填的 05:00", restart: true }, + { packet: fixtures.midpoint, adjustable: false, exported: "按范围中点 05:00", restart: false }, + ]) { + const h = createClientLifecycleHarness(); + const originalFetch = globalThis.fetch; + let packets = 0; + let saved = false; + let submitted: Record | undefined; + let packet = scenario.packet; + globalThis.fetch = async (url, request) => { + const path = String(url); + if (path.startsWith("/api/chart-choice?")) { + return Response.json(startFor(packet, scenario.adjustable && !saved, (scenario.restart || saved) ? currentRecord(packet) : undefined)); + } + const body = JSON.parse(String(request?.body)); + if (path === "/api/chart-choice/packet") { + packets += 1; + assert.deepEqual(Object.keys(body).sort(), body.window ? ["adaptive", "subjectId", "window"] : ["adaptive", "subjectId"]); + if (body.window) { + assert.deepEqual(body.window, fixtures.declared.window); + packet = fixtures.declared; + } + return Response.json(packet); + } + assert.equal(path, "/api/chart-choice/adopt"); + saved = true; + submitted = body; + return Response.json({ decision: { saved_at: "1997-08-08T05:00" } }); + }; + try { + await h.render(); + const waitFor = async (text: string) => { + for (let i = 0; i < 10 && !h.elements().some((element) => element.text === text); i += 1) await h.idle(); + assert.ok(h.elements().some((element) => element.text === text), text); + }; + if (scenario.restart) { + await waitFor("重新纠正"); + assert.equal(packets, 0); + await h.event(h.elements().find((element) => element.text === "重新纠正")!); + } + await waitFor(scenario.exported); + const elements = h.elements(); + assert.equal(elements.filter((element) => element.text === "用 05:00" && element.tagName === "BUTTON").length, 1); + assert.equal(elements.some((element) => /开始答题|先到这里|下一题|都没有|记不清|换另一侧|更像哪一种|你提到的事/.test(element.text)), false); + assert.equal(elements.filter((element) => element.tagName === "DT").length, 6); + assert.equal(elements.some((element) => element.getAttribute("role") === "radiogroup"), scenario.adjustable); + if (scenario.adjustable) { + assert.ok(elements.some((element) => element.text.includes("前后 15 分钟试算里没有变化"))); + await h.event(elements.find((element) => element.getAttribute("role") === "radio" && element.text === "30 分钟")!); + await waitFor(scenario.exported); + assert.ok(h.elements().some((element) => element.text.includes("在你填的范围里没有换座"))); + } + await h.event(h.elements().find((element) => element.tagName === "BUTTON" && element.text === "用 05:00")!); + await waitFor("当前使用"); + assert.deepEqual(submitted, { subjectId: "self", adaptive: true, mode: "choice", + ...(scenario.adjustable ? { window: fixtures.declared.window, rangeChosen: true } : {}), + }); + assert.equal(packets, scenario.adjustable ? 2 : 1); + assert.equal(h.errors.length, 0); + } finally { + globalThis.fetch = originalFetch; + await h.close(); + } + } +}); + +test("default packet and adoption use the native completion without evidence and preserve certain and wide ranges", async () => { + const { readDefaultChartChoice, adoptDefaultChartChoice } = await import("../src/lib/chart-choice/server"); + const profile = { + name: "虚构人物", birth_date: "1997-08-08", reported_birth_time: "05:00", birth_time_source: "approximate", + birth_time_status: "reported", declared_window_start: "04:30", declared_window_end: "05:30", + uncertainty_before_minutes: 30, uncertainty_after_minutes: 30, birth_time_uncertainty_known: true, + latitude: 36.42, longitude: 114.21, timezone_offset: 8, timezone_id: "Asia/Shanghai", birth_place_label: "虚构市", + }; + for (const { row, packet, rule, request, checkWide } of [ + { row: profile, packet: fixtures.declared, rule: "reported_time_default", request: {}, checkWide: false }, + { row: { ...profile, declared_window_start: null, declared_window_end: null, uncertainty_before_minutes: 0, + uncertainty_after_minutes: 0, birth_time_uncertainty_known: false, birth_time_source: "family_exact" }, + packet: fixtures.declared, rule: "reported_time_default", + request: { window: fixtures.declared.window, rangeChosen: true }, checkWide: false }, + { row: { ...profile, reported_birth_time: null, birth_time_source: "period_only", birth_time_precision_kind: "period", + declared_window_start: "02:00", declared_window_end: "08:00", uncertainty_before_minutes: null, uncertainty_after_minutes: null }, + packet: fixtures.wideMidpoint, rule: "window_midpoint_default", request: {}, checkWide: true }, + ]) { + const caller = { from: () => ({ select: () => ({ eq: () => ({ maybeSingle: async () => ({ data: row, error: null }) }) }) }) }; + const fetchEngine: typeof fetch = async (url, init) => { + assert.match(String(url), /\/api\/chart_choice\/decide$/); + const body = JSON.parse(String(init?.body)); + assert.equal(body.adaptive, true); + assert.equal(body.stop, true); + assert.equal(body.chart_answers ?? body.answers ?? body.resolved, undefined); + if (checkWide) { + assert.equal(body.start_at, "1997-08-08T04:00"); + assert.equal(body.end_at, "1997-08-08T06:00"); + assert.deepEqual(body.reported_default_window, packet.decision.reported_choice!.segment); + } + return Response.json(packet); + }; + const result = await readDefaultChartChoice(caller, "fixture", request, fetchEngine); + assert.equal(result.status, "complete"); + assert.equal(result.reported_known, !checkWide); + assert.deepEqual(result.window, packet.decision.reported_choice!.segment); + const admin = { rpc: async (fn: string, args: Record) => { + assert.equal(fn, "chart_choice_adopt_v4"); + assert.equal((args.p_provenance as { source: string }).source, rule); + assert.equal(args.p_window_start, request.rangeChosen ? "04:30" : null); + assert.equal(args.p_window_end, request.rangeChosen ? "05:30" : null); + assert.equal(args.p_time, "05:00:00"); + return { data: {}, error: null }; + } }; + assert.equal((await adoptDefaultChartChoice(caller, admin, "fixture", request, fetchEngine)).decision.rule, rule); + } + const { legacy, default: native } = fixtures.certain; + const caller = { from: () => ({ select: () => ({ eq: () => ({ maybeSingle: async () => ({ data: { + ...profile, reported_birth_time: legacy.reported_at.slice(11), declared_window_start: legacy.window.start_at.slice(11), + declared_window_end: legacy.window.end_at.slice(11), uncertainty_before_minutes: 1, uncertainty_after_minutes: 1, + }, error: null }) }) }) }) }; + const paths: string[] = []; + const certain = await readDefaultChartChoice(caller, "fixture", {}, async (url) => { + const path = String(url).split("/").at(-1)!; + paths.push(path); + return Response.json(path === "packet" ? legacy : native); + }); + assert.equal(certain.status, "certain"); + assert.deepEqual(paths, ["decide", "packet"]); + assert.equal(certain.saved_at, legacy.saved_at); +}); + +test("chart choice API rejects questionnaire, resume, side and evidence inputs with an explicit 400", () => { + const program = ` + import assert from "node:assert/strict"; + import { mock } from "node:test"; + mock.module("server-only", { namedExports: {} }); + mock.module("@/lib/supabase/server", { namedExports: { + createServerSupabaseClient: async () => ({auth:{getUser:async () => ({data:{user:{id:"fixture"}}})}}), + }}); + mock.module("@/lib/supabase/admin", { namedExports: { createAdminSupabaseClient: () => { throw new Error("adoption must not run"); } } }); + mock.module("@/lib/product-access", { namedExports: { isProductEnabled: async () => true } }); + mock.module("@/lib/request-rate-limit", { namedExports: { consumeUserRequestRateLimit: () => ({ok:true}) } }); + const packet = (await import("./src/app/api/chart-choice/packet/route.ts")).POST; + const adopt = (await import("./src/app/api/chart-choice/adopt/route.ts")).POST; + const astrologer = (await import("./src/app/api/chart-choice/astrologer/route.ts")).POST; + const run = async (handler, body) => { + const response = await handler(new Request("http://localhost/api/chart-choice", {method:"POST",body:JSON.stringify(body)})); + assert.equal(response.status, 400, JSON.stringify(body)); + assert.equal((await response.json()).code, "questionnaire_paused"); + }; + for (const handler of [packet, adopt]) { + for (const body of [{questionnaire:true}, {chart_answers:{}}, {answers:{}}, {overrideSide:"before"}, + {resolved:[{}]}, {resumeRecordId:"old-record"}, {event_answers:{}}, {appearance:"before"}, {birthmarks:[]}, + {readBothAll:true}]) await run(handler, body); + } + await run(astrologer, {input:{}}); + `; + const run = spawnSync(process.execPath, ["--experimental-test-module-mocks", "--import", "tsx", "--input-type=module", "--eval", program], { + cwd: new URL("..", import.meta.url), encoding: "utf8", timeout: 15_000, + }); + assert.equal(run.status, 0, run.stderr || run.stdout); +}); diff --git a/frontend/tests/fixtures/chart-choice-pause-native-20261115.json b/frontend/tests/fixtures/chart-choice-pause-native-20261115.json new file mode 100644 index 00000000..dd8668df --- /dev/null +++ b/frontend/tests/fixtures/chart-choice-pause-native-20261115.json @@ -0,0 +1,1319 @@ +{ + "source": "Native scripts.chart_choice.service using fictional test declarations", + "defaultTrial": { + "engine_version": "chart-choice-v2", + "window": { + "start_at": "1997-08-08T04:45", + "end_at": "1997-08-08T05:15" + }, + "reported_at": "1997-08-08T05:00", + "reported_known": true, + "ayanamsa": "lahiri", + "node_mode": "true", + "status": "complete", + "segment": { + "start_at": "1997-08-08T04:45:00", + "end_at": "1997-08-08T05:15:00" + }, + "remaining_key_lines": 4, + "ordered_lines": [ + { + "at": "1997-08-08T04:58:37", + "degree": 13.333333, + "charts": [ + "D9" + ] + }, + { + "at": "1997-08-08T05:06:50", + "degree": 15.0, + "charts": [ + "D10", + "D2", + "D6", + "D8", + "D4", + "D12", + "D24" + ] + }, + { + "at": "1997-08-08T04:52:04", + "degree": 12.0, + "charts": [ + "D10" + ] + }, + { + "at": "1997-08-08T04:48:22", + "degree": 11.25, + "charts": [ + "D8", + "D24" + ] + } + ], + "minor_lines": [ + { + "at": "1997-08-08T04:54:31", + "charts": [ + "D12", + "D24" + ] + }, + { + "at": "1997-08-08T05:00:41", + "charts": [ + "D24" + ] + }, + { + "at": "1997-08-08T05:13:00", + "charts": [ + "D24" + ] + } + ], + "question_charts": [ + { + "chart": "D9", + "possibilities": 2 + }, + { + "chart": "D10", + "possibilities": 3 + }, + { + "chart": "D2", + "possibilities": 2 + }, + { + "chart": "D6", + "possibilities": 2 + }, + { + "chart": "D8", + "possibilities": 3 + } + ], + "chart_questions": [], + "confirmed_charts": 1, + "saved_at": "1997-08-08T05:00", + "decision": { + "saved_at": "1997-08-08T05:00", + "segment": { + "start_at": "1997-08-08T04:45:00", + "end_at": "1997-08-08T05:15:00" + }, + "rounds": [], + "undetermined_charts": [ + "D9", + "D10", + "D2", + "D6", + "D8" + ], + "events": [], + "key_charts": [ + { + "chart": "D1", + "sign": "巨蟹", + "decided_by": null + }, + { + "chart": "D9", + "sign": "天蝎", + "decided_by": null, + "flips_at": "1997-08-08T04:58:37" + }, + { + "chart": "D10", + "sign": "巨蟹", + "decided_by": null, + "flips_at": "1997-08-08T04:52:04" + }, + { + "chart": "D2", + "sign": "巨蟹", + "decided_by": null, + "flips_at": "1997-08-08T05:06:50" + }, + { + "chart": "D6", + "sign": "射手", + "decided_by": null, + "flips_at": "1997-08-08T05:06:50" + }, + { + "chart": "D8", + "sign": "巨蟹", + "decided_by": null, + "flips_at": "1997-08-08T04:48:22" + } + ], + "events_detail": [], + "evidence_summary": { + "event_count": 0, + "matched": 0, + "confidence": "tendency_only", + "coverage": { + "event_count": 0, + "category_count": 0, + "group_count": 0, + "groups": [], + "year_span": 0, + "level": "thin" + }, + "caveat": "这一步按占星师的方法计算,结果只作参考,不是验证结果。2026-11-06 我们在 77 个出生时间有可靠记录的案例上回测过同类的事件打分:30 分钟范围里头名命中 19%,随机挑也是 20%,直接用报的时间是 34%。" + }, + "remaining_key_lines": 4, + "has_next": false, + "rule": "stopped", + "reported_choice": { + "saved_at": "1997-08-08T05:00", + "segment": { + "start_at": "1997-08-08T04:45", + "end_at": "1997-08-08T05:15" + }, + "reported_known": true, + "key_charts": [ + { + "chart": "D1", + "sign": "巨蟹", + "decided_by": null + }, + { + "chart": "D9", + "sign": "天蝎", + "decided_by": null, + "flips_at": "1997-08-08T04:58:37" + }, + { + "chart": "D10", + "sign": "巨蟹", + "decided_by": null, + "flips_at": "1997-08-08T04:52:04" + }, + { + "chart": "D2", + "sign": "巨蟹", + "decided_by": null, + "flips_at": "1997-08-08T05:06:50" + }, + { + "chart": "D6", + "sign": "射手", + "decided_by": null, + "flips_at": "1997-08-08T05:06:50" + }, + { + "chart": "D8", + "sign": "巨蟹", + "decided_by": null, + "flips_at": "1997-08-08T04:48:22" + } + ], + "undetermined_charts": [ + "D9", + "D10", + "D2", + "D6", + "D8" + ], + "remaining_key_lines": 4 + }, + "asked": [], + "conflict": [], + "resolved": [], + "stop_reason": "stopped" + } + }, + "declared": { + "engine_version": "chart-choice-v2", + "window": { + "start_at": "1997-08-08T04:30", + "end_at": "1997-08-08T05:30" + }, + "reported_at": "1997-08-08T05:00", + "reported_known": true, + "ayanamsa": "lahiri", + "node_mode": "true", + "status": "complete", + "segment": { + "start_at": "1997-08-08T04:30:00", + "end_at": "1997-08-08T05:30:00" + }, + "remaining_key_lines": 9, + "ordered_lines": [ + { + "at": "1997-08-08T04:58:37", + "degree": 13.333333, + "charts": [ + "D9" + ] + }, + { + "at": "1997-08-08T05:15:03", + "degree": 16.666667, + "charts": [ + "D9" + ] + }, + { + "at": "1997-08-08T04:42:14", + "degree": 10.0, + "charts": [ + "D9", + "D6", + "D12", + "D24" + ] + }, + { + "at": "1997-08-08T05:06:50", + "degree": 15.0, + "charts": [ + "D10", + "D2", + "D6", + "D8", + "D4", + "D12", + "D24" + ] + }, + { + "at": "1997-08-08T04:52:04", + "degree": 12.0, + "charts": [ + "D10" + ] + }, + { + "at": "1997-08-08T05:21:38", + "degree": 18.0, + "charts": [ + "D10" + ] + }, + { + "at": "1997-08-08T04:37:19", + "degree": 9.0, + "charts": [ + "D10" + ] + }, + { + "at": "1997-08-08T04:48:22", + "degree": 11.25, + "charts": [ + "D8", + "D24" + ] + }, + { + "at": "1997-08-08T05:25:20", + "degree": 18.75, + "charts": [ + "D8", + "D24" + ] + } + ], + "minor_lines": [ + { + "at": "1997-08-08T04:36:06", + "charts": [ + "D24" + ] + }, + { + "at": "1997-08-08T04:54:31", + "charts": [ + "D12", + "D24" + ] + }, + { + "at": "1997-08-08T05:00:41", + "charts": [ + "D24" + ] + }, + { + "at": "1997-08-08T05:13:00", + "charts": [ + "D24" + ] + }, + { + "at": "1997-08-08T05:19:10", + "charts": [ + "D12", + "D24" + ] + } + ], + "question_charts": [ + { + "chart": "D9", + "possibilities": 4 + }, + { + "chart": "D10", + "possibilities": 5 + }, + { + "chart": "D2", + "possibilities": 2 + }, + { + "chart": "D6", + "possibilities": 3 + }, + { + "chart": "D8", + "possibilities": 4 + } + ], + "chart_questions": [], + "confirmed_charts": 1, + "saved_at": "1997-08-08T05:00", + "decision": { + "saved_at": "1997-08-08T05:00", + "segment": { + "start_at": "1997-08-08T04:30:00", + "end_at": "1997-08-08T05:30:00" + }, + "rounds": [], + "undetermined_charts": [ + "D9", + "D10", + "D2", + "D6", + "D8" + ], + "events": [], + "key_charts": [ + { + "chart": "D1", + "sign": "巨蟹", + "decided_by": null + }, + { + "chart": "D9", + "sign": "天蝎", + "decided_by": null, + "flips_at": "1997-08-08T04:42:14" + }, + { + "chart": "D10", + "sign": "巨蟹", + "decided_by": null, + "flips_at": "1997-08-08T04:37:19" + }, + { + "chart": "D2", + "sign": "巨蟹", + "decided_by": null, + "flips_at": "1997-08-08T05:06:50" + }, + { + "chart": "D6", + "sign": "射手", + "decided_by": null, + "flips_at": "1997-08-08T04:42:14" + }, + { + "chart": "D8", + "sign": "巨蟹", + "decided_by": null, + "flips_at": "1997-08-08T04:48:22" + } + ], + "events_detail": [], + "evidence_summary": { + "event_count": 0, + "matched": 0, + "confidence": "tendency_only", + "coverage": { + "event_count": 0, + "category_count": 0, + "group_count": 0, + "groups": [], + "year_span": 0, + "level": "thin" + }, + "caveat": "这一步按占星师的方法计算,结果只作参考,不是验证结果。2026-11-06 我们在 77 个出生时间有可靠记录的案例上回测过同类的事件打分:30 分钟范围里头名命中 19%,随机挑也是 20%,直接用报的时间是 34%。" + }, + "remaining_key_lines": 9, + "has_next": false, + "rule": "stopped", + "reported_choice": { + "saved_at": "1997-08-08T05:00", + "segment": { + "start_at": "1997-08-08T04:30", + "end_at": "1997-08-08T05:30" + }, + "reported_known": true, + "key_charts": [ + { + "chart": "D1", + "sign": "巨蟹", + "decided_by": null + }, + { + "chart": "D9", + "sign": "天蝎", + "decided_by": null, + "flips_at": "1997-08-08T04:42:14" + }, + { + "chart": "D10", + "sign": "巨蟹", + "decided_by": null, + "flips_at": "1997-08-08T04:37:19" + }, + { + "chart": "D2", + "sign": "巨蟹", + "decided_by": null, + "flips_at": "1997-08-08T05:06:50" + }, + { + "chart": "D6", + "sign": "射手", + "decided_by": null, + "flips_at": "1997-08-08T04:42:14" + }, + { + "chart": "D8", + "sign": "巨蟹", + "decided_by": null, + "flips_at": "1997-08-08T04:48:22" + } + ], + "undetermined_charts": [ + "D9", + "D10", + "D2", + "D6", + "D8" + ], + "remaining_key_lines": 9 + }, + "asked": [], + "conflict": [], + "resolved": [], + "stop_reason": "stopped" + } + }, + "midpoint": { + "engine_version": "chart-choice-v2", + "window": { + "start_at": "1997-08-08T04:40", + "end_at": "1997-08-08T05:20" + }, + "reported_at": "1997-08-08T05:00", + "reported_known": false, + "ayanamsa": "lahiri", + "node_mode": "true", + "status": "complete", + "segment": { + "start_at": "1997-08-08T04:40:00", + "end_at": "1997-08-08T05:20:00" + }, + "remaining_key_lines": 6, + "ordered_lines": [ + { + "at": "1997-08-08T04:58:37", + "degree": 13.333333, + "charts": [ + "D9" + ] + }, + { + "at": "1997-08-08T05:15:03", + "degree": 16.666667, + "charts": [ + "D9" + ] + }, + { + "at": "1997-08-08T04:42:14", + "degree": 10.0, + "charts": [ + "D9", + "D6", + "D12", + "D24" + ] + }, + { + "at": "1997-08-08T05:06:50", + "degree": 15.0, + "charts": [ + "D10", + "D2", + "D6", + "D8", + "D4", + "D12", + "D24" + ] + }, + { + "at": "1997-08-08T04:52:04", + "degree": 12.0, + "charts": [ + "D10" + ] + }, + { + "at": "1997-08-08T04:48:22", + "degree": 11.25, + "charts": [ + "D8", + "D24" + ] + } + ], + "minor_lines": [ + { + "at": "1997-08-08T04:54:31", + "charts": [ + "D12", + "D24" + ] + }, + { + "at": "1997-08-08T05:00:41", + "charts": [ + "D24" + ] + }, + { + "at": "1997-08-08T05:13:00", + "charts": [ + "D24" + ] + }, + { + "at": "1997-08-08T05:19:10", + "charts": [ + "D12", + "D24" + ] + } + ], + "question_charts": [ + { + "chart": "D9", + "possibilities": 4 + }, + { + "chart": "D10", + "possibilities": 3 + }, + { + "chart": "D2", + "possibilities": 2 + }, + { + "chart": "D6", + "possibilities": 3 + }, + { + "chart": "D8", + "possibilities": 3 + } + ], + "chart_questions": [], + "confirmed_charts": 1, + "saved_at": "1997-08-08T05:00", + "decision": { + "saved_at": "1997-08-08T05:00", + "segment": { + "start_at": "1997-08-08T04:40:00", + "end_at": "1997-08-08T05:20:00" + }, + "rounds": [], + "undetermined_charts": [ + "D9", + "D10", + "D2", + "D6", + "D8" + ], + "events": [], + "key_charts": [ + { + "chart": "D1", + "sign": "巨蟹", + "decided_by": null + }, + { + "chart": "D9", + "sign": "天蝎", + "decided_by": null, + "flips_at": "1997-08-08T04:42:14" + }, + { + "chart": "D10", + "sign": "巨蟹", + "decided_by": null, + "flips_at": "1997-08-08T04:52:04" + }, + { + "chart": "D2", + "sign": "巨蟹", + "decided_by": null, + "flips_at": "1997-08-08T05:06:50" + }, + { + "chart": "D6", + "sign": "射手", + "decided_by": null, + "flips_at": "1997-08-08T04:42:14" + }, + { + "chart": "D8", + "sign": "巨蟹", + "decided_by": null, + "flips_at": "1997-08-08T04:48:22" + } + ], + "events_detail": [], + "evidence_summary": { + "event_count": 0, + "matched": 0, + "confidence": "tendency_only", + "coverage": { + "event_count": 0, + "category_count": 0, + "group_count": 0, + "groups": [], + "year_span": 0, + "level": "thin" + }, + "caveat": "这一步按占星师的方法计算,结果只作参考,不是验证结果。2026-11-06 我们在 77 个出生时间有可靠记录的案例上回测过同类的事件打分:30 分钟范围里头名命中 19%,随机挑也是 20%,直接用报的时间是 34%。" + }, + "remaining_key_lines": 6, + "has_next": false, + "rule": "stopped", + "reported_choice": { + "saved_at": "1997-08-08T05:00", + "segment": { + "start_at": "1997-08-08T04:40", + "end_at": "1997-08-08T05:20" + }, + "reported_known": false, + "key_charts": [ + { + "chart": "D1", + "sign": "巨蟹", + "decided_by": null + }, + { + "chart": "D9", + "sign": "天蝎", + "decided_by": null, + "flips_at": "1997-08-08T04:42:14" + }, + { + "chart": "D10", + "sign": "巨蟹", + "decided_by": null, + "flips_at": "1997-08-08T04:52:04" + }, + { + "chart": "D2", + "sign": "巨蟹", + "decided_by": null, + "flips_at": "1997-08-08T05:06:50" + }, + { + "chart": "D6", + "sign": "射手", + "decided_by": null, + "flips_at": "1997-08-08T04:42:14" + }, + { + "chart": "D8", + "sign": "巨蟹", + "decided_by": null, + "flips_at": "1997-08-08T04:48:22" + } + ], + "undetermined_charts": [ + "D9", + "D10", + "D2", + "D6", + "D8" + ], + "remaining_key_lines": 6 + }, + "asked": [], + "conflict": [], + "resolved": [], + "stop_reason": "stopped" + } + }, + "wideMidpoint": { + "engine_version": "chart-choice-v2", + "window": { + "start_at": "1997-08-08T04:00", + "end_at": "1997-08-08T06:00" + }, + "reported_at": "1997-08-08T05:00", + "reported_known": true, + "ayanamsa": "lahiri", + "node_mode": "true", + "status": "complete", + "segment": { + "start_at": "1997-08-08T04:00:00", + "end_at": "1997-08-08T06:00:00" + }, + "remaining_key_lines": 22, + "ordered_lines": [ + { + "at": "1997-08-08T04:58:37", + "degree": 13.333333, + "charts": [ + "D9" + ] + }, + { + "at": "1997-08-08T05:15:03", + "degree": 16.666667, + "charts": [ + "D9" + ] + }, + { + "at": "1997-08-08T04:42:14", + "degree": 10.0, + "charts": [ + "D9", + "D6", + "D12", + "D24" + ] + }, + { + "at": "1997-08-08T05:31:30", + "degree": 20.0, + "charts": [ + "D9", + "D6", + "D12", + "D24" + ] + }, + { + "at": "1997-08-08T04:25:54", + "degree": 6.666667, + "charts": [ + "D9" + ] + }, + { + "at": "1997-08-08T05:47:56", + "degree": 23.333333, + "charts": [ + "D9" + ] + }, + { + "at": "1997-08-08T04:09:38", + "degree": 3.333333, + "charts": [ + "D9" + ] + }, + { + "at": "1997-08-08T05:06:50", + "degree": 15.0, + "charts": [ + "D10", + "D2", + "D6", + "D8", + "D4", + "D12", + "D24" + ] + }, + { + "at": "1997-08-08T04:52:04", + "degree": 12.0, + "charts": [ + "D10" + ] + }, + { + "at": "1997-08-08T05:21:38", + "degree": 18.0, + "charts": [ + "D10" + ] + }, + { + "at": "1997-08-08T04:37:19", + "degree": 9.0, + "charts": [ + "D10" + ] + }, + { + "at": "1997-08-08T05:36:26", + "degree": 21.0, + "charts": [ + "D10" + ] + }, + { + "at": "1997-08-08T04:22:38", + "degree": 6.0, + "charts": [ + "D10" + ] + }, + { + "at": "1997-08-08T05:51:14", + "degree": 24.0, + "charts": [ + "D10" + ] + }, + { + "at": "1997-08-08T04:08:01", + "degree": 3.0, + "charts": [ + "D10" + ] + }, + { + "at": "1997-08-08T04:48:22", + "degree": 11.25, + "charts": [ + "D8", + "D24" + ] + }, + { + "at": "1997-08-08T05:25:20", + "degree": 18.75, + "charts": [ + "D8", + "D24" + ] + }, + { + "at": "1997-08-08T04:29:58", + "degree": 7.5, + "charts": [ + "D8", + "D4", + "D12", + "D24" + ] + }, + { + "at": "1997-08-08T04:17:45", + "degree": 5.0, + "charts": [ + "D6", + "D12", + "D24" + ] + }, + { + "at": "1997-08-08T05:43:50", + "degree": 22.5, + "charts": [ + "D8", + "D4", + "D12", + "D24" + ] + }, + { + "at": "1997-08-08T04:11:40", + "degree": 3.75, + "charts": [ + "D8", + "D24" + ] + }, + { + "at": "1997-08-08T05:56:09", + "degree": 25.0, + "charts": [ + "D6", + "D12", + "D24" + ] + } + ], + "minor_lines": [ + { + "at": "1997-08-08T04:05:35", + "charts": [ + "D12", + "D24" + ] + }, + { + "at": "1997-08-08T04:23:51", + "charts": [ + "D24" + ] + }, + { + "at": "1997-08-08T04:36:06", + "charts": [ + "D24" + ] + }, + { + "at": "1997-08-08T04:54:31", + "charts": [ + "D12", + "D24" + ] + }, + { + "at": "1997-08-08T05:00:41", + "charts": [ + "D24" + ] + }, + { + "at": "1997-08-08T05:13:00", + "charts": [ + "D24" + ] + }, + { + "at": "1997-08-08T05:19:10", + "charts": [ + "D12", + "D24" + ] + }, + { + "at": "1997-08-08T05:37:40", + "charts": [ + "D24" + ] + }, + { + "at": "1997-08-08T05:50:00", + "charts": [ + "D24" + ] + } + ], + "question_charts": [ + { + "chart": "D9", + "possibilities": 8 + }, + { + "chart": "D10", + "possibilities": 9 + }, + { + "chart": "D2", + "possibilities": 2 + }, + { + "chart": "D6", + "possibilities": 6 + }, + { + "chart": "D8", + "possibilities": 7 + } + ], + "chart_questions": [], + "confirmed_charts": 1, + "saved_at": "1997-08-08T05:00", + "decision": { + "saved_at": "1997-08-08T05:00", + "segment": { + "start_at": "1997-08-08T04:00:00", + "end_at": "1997-08-08T06:00:00" + }, + "rounds": [], + "undetermined_charts": [ + "D9", + "D10", + "D2", + "D6", + "D8" + ], + "events": [], + "key_charts": [ + { + "chart": "D1", + "sign": "巨蟹", + "decided_by": null + }, + { + "chart": "D9", + "sign": "天蝎", + "decided_by": null, + "flips_at": "1997-08-08T04:09:38" + }, + { + "chart": "D10", + "sign": "巨蟹", + "decided_by": null, + "flips_at": "1997-08-08T04:08:01" + }, + { + "chart": "D2", + "sign": "巨蟹", + "decided_by": null, + "flips_at": "1997-08-08T05:06:50" + }, + { + "chart": "D6", + "sign": "射手", + "decided_by": null, + "flips_at": "1997-08-08T04:17:45" + }, + { + "chart": "D8", + "sign": "巨蟹", + "decided_by": null, + "flips_at": "1997-08-08T04:11:40" + } + ], + "events_detail": [], + "evidence_summary": { + "event_count": 0, + "matched": 0, + "confidence": "tendency_only", + "coverage": { + "event_count": 0, + "category_count": 0, + "group_count": 0, + "groups": [], + "year_span": 0, + "level": "thin" + }, + "caveat": "这一步按占星师的方法计算,结果只作参考,不是验证结果。2026-11-06 我们在 77 个出生时间有可靠记录的案例上回测过同类的事件打分:30 分钟范围里头名命中 19%,随机挑也是 20%,直接用报的时间是 34%。" + }, + "remaining_key_lines": 22, + "has_next": false, + "rule": "stopped", + "reported_choice": { + "saved_at": "1997-08-08T05:00", + "segment": { + "start_at": "1997-08-08T02:00", + "end_at": "1997-08-08T08:00" + }, + "reported_known": true, + "key_charts": [ + { + "chart": "D1", + "sign": "巨蟹", + "decided_by": null, + "flips_at": "1997-08-08T03:53:28" + }, + { + "chart": "D9", + "sign": "天蝎", + "decided_by": null, + "flips_at": "1997-08-08T02:05:30" + }, + { + "chart": "D10", + "sign": "巨蟹", + "decided_by": null, + "flips_at": "1997-08-08T02:02:36" + }, + { + "chart": "D2", + "sign": "巨蟹", + "decided_by": null, + "flips_at": "1997-08-08T02:42:45" + }, + { + "chart": "D6", + "sign": "射手", + "decided_by": null, + "flips_at": "1997-08-08T02:20:11" + }, + { + "chart": "D8", + "sign": "巨蟹", + "decided_by": null, + "flips_at": "1997-08-08T02:09:09" + } + ], + "undetermined_charts": [ + "D1", + "D9", + "D10", + "D2", + "D6", + "D8" + ], + "remaining_key_lines": 65 + }, + "asked": [], + "conflict": [], + "resolved": [], + "stop_reason": "stopped" + } + }, + "certain": { + "legacy": { + "engine_version": "chart-choice-v1", + "window": { + "start_at": "1997-08-08T05:01", + "end_at": "1997-08-08T05:03" + }, + "reported_at": "1997-08-08T05:02", + "reported_known": true, + "ayanamsa": "lahiri", + "node_mode": "true", + "status": "certain", + "saved_at": "1997-08-08T05:02" + }, + "default": { + "engine_version": "chart-choice-v2", + "window": { + "start_at": "1997-08-08T05:01", + "end_at": "1997-08-08T05:03" + }, + "reported_at": "1997-08-08T05:02", + "reported_known": true, + "ayanamsa": "lahiri", + "node_mode": "true", + "status": "complete", + "segment": { + "start_at": "1997-08-08T05:01:00", + "end_at": "1997-08-08T05:03:00" + }, + "remaining_key_lines": 0, + "ordered_lines": [], + "minor_lines": [], + "question_charts": [], + "chart_questions": [], + "confirmed_charts": 6, + "saved_at": "1997-08-08T05:02", + "decision": { + "saved_at": "1997-08-08T05:02", + "segment": { + "start_at": "1997-08-08T05:01:00", + "end_at": "1997-08-08T05:03:00" + }, + "rounds": [], + "undetermined_charts": [], + "events": [], + "key_charts": [ + { + "chart": "D1", + "sign": "巨蟹", + "decided_by": null + }, + { + "chart": "D9", + "sign": "天蝎", + "decided_by": null + }, + { + "chart": "D10", + "sign": "巨蟹", + "decided_by": null + }, + { + "chart": "D2", + "sign": "巨蟹", + "decided_by": null + }, + { + "chart": "D6", + "sign": "射手", + "decided_by": null + }, + { + "chart": "D8", + "sign": "巨蟹", + "decided_by": null + } + ], + "events_detail": [], + "evidence_summary": { + "event_count": 0, + "matched": 0, + "confidence": "tendency_only", + "coverage": { + "event_count": 0, + "category_count": 0, + "group_count": 0, + "groups": [], + "year_span": 0, + "level": "thin" + }, + "caveat": "这一步按占星师的方法计算,结果只作参考,不是验证结果。2026-11-06 我们在 77 个出生时间有可靠记录的案例上回测过同类的事件打分:30 分钟范围里头名命中 19%,随机挑也是 20%,直接用报的时间是 34%。" + }, + "remaining_key_lines": 0, + "has_next": false, + "rule": "stopped", + "reported_choice": { + "saved_at": "1997-08-08T05:02", + "segment": { + "start_at": "1997-08-08T05:01", + "end_at": "1997-08-08T05:03" + }, + "reported_known": true, + "key_charts": [ + { + "chart": "D1", + "sign": "巨蟹", + "decided_by": null + }, + { + "chart": "D9", + "sign": "天蝎", + "decided_by": null + }, + { + "chart": "D10", + "sign": "巨蟹", + "decided_by": null + }, + { + "chart": "D2", + "sign": "巨蟹", + "decided_by": null + }, + { + "chart": "D6", + "sign": "射手", + "decided_by": null + }, + { + "chart": "D8", + "sign": "巨蟹", + "decided_by": null + } + ], + "undetermined_charts": [], + "remaining_key_lines": 0 + }, + "asked": [], + "conflict": [], + "resolved": [], + "stop_reason": "stopped" + } + } + } +}