Compare commits

..
5 changed files with 147 additions and 0 deletions
+6
View File
@@ -1,5 +1,11 @@
# BLOCKED
## BUG-1865 · 报告候选范围缺少采纳凭据(#227,2026-11-13)
- 缺什么:旧流程的待确认区间与已采纳区间在报告读取接口中返回相同的起止时间字段。按任务书,需先明确报告以什么证据确认范围已采纳。
- 替代证据:用隔离 PostgreSQL 17 执行当前 RPC 定义,在合成记录上验证两类结果相同。详见 [#227 进度](docs/tasks/PROGRESS-reading-rectified-window-20261113.md)。
- 解除:拍板后,给报告提供可核对的采纳凭据,并覆盖旧流程待确认记录与实际采纳记录的分流。
## TASK-voice-reading-20261027:真实模型输出没看(issue #113,2026-10-27)
- **报告解读**:本机没有 `DS_KEY`、`BACKTEST_MODEL_KEY`、`OPENAI_API_KEY`、`ANTHROPIC_API_KEY`。解读的形状、校验、失败兜底、记账都用假模型测过;真实模型写出来的解读是什么样、几成能一次过校验、耗时多少,都没测,不得写成通过。
+13
View File
@@ -25157,3 +25157,16 @@
- 防复发:分别核对边界行的状态、两侧误差及来源,并验证临界分盘只影响依赖它的结论。
- 关联记录:#224、#144、BUG-1858、BUG-1848。
- 修复版本:`codex/reading-birth-window-20261113`,待发布。
## BUG-1865 | 报告不能用候选区间返回值确认生时校正已采纳
- 状态:blocked(#227,待产品拍板)。
- 首次发现 / 最近更新:2026-11-13 / 2026-11-13。
- 影响面:个人报告的出生资料来源、时间误差范围与证据等级。
- 现象:报告附录能读到校正区间,解读仍可用默认误差范围。将当前候选区间直接作为采纳区间,会把旧流程中待确认的范围用于证据判断。
- 触发条件:报告读取一个带候选起止的旧流程 case,状态为 `completed`,候选仍为 `pending_validation`。
- 根因:`read_report_candidate_range` 的旧流程分支同时读取 `completed` 和 `confirmed`,只返回起止时间,与新流程已采纳普通区间同形。报告的误差范围逻辑原先只读填写记录。
- 验证:隔离 PostgreSQL 17 按当前 RPC 定义核对合成记录,待确认旧区间、已确认旧区间和新流程已采纳普通区间的返回值一致;新流程仅候选就绪时返回空。详见 `docs/tasks/PROGRESS-reading-rectified-window-20261113.md`。
- 防复发:补充报告入口的采纳来源与当前计算时刻绑定回归。保持候选范围读取的权限边界(BUG-526)。
- 关联记录:BUG-1864、BUG-568、BUG-526、BUG-982、BUG-983。
- 修复版本:待拍板。
@@ -0,0 +1,33 @@
# #227 需拍板答复:生时纠正区间从选盘页的采纳记录取,不走旧接口(2026-11-13,Claude)
本文件补充 `TASK-reading-rectified-window-20261113.md`,两者冲突时以本文件为准。
## 结论
你的诊断是对的:`read_report_candidate_range` 分不清「待确认」和「已采纳」,不能直接用。但现在的生时纠正(11-06 改版,BUG-1644/48/49/54,选盘页)本来就不走这个接口。采纳时,`chart_choice_adopt_v2/v3` 会写档案:
- `birth_time_status = 'accepted'`,`active_birth_time` 等于 `decision.saved_at`;
- `active_birth_provenance`,内容为 `contract: "chart-choice-v1"`、`window`(`start_at`/`end_at`)、`line_at`、`line_charts`、`chosen_side`、`both_versions`、`adopted_at` 等,完整字段见 `frontend/src/lib/chart-choice/server.ts::adoptChartChoice`。
这份记录本身就是「已采纳、对应当前出生资料」的凭据。写入声明范围时会清掉旧的采纳(`guard_adopted_birth_date` / `chart_profiles_drop_adopted_on_declaration`)。
**授权的做法,取值按以下顺序:**
1. **选盘页采纳**,`source = "rectified_range"`。条件:`birth_time_status` 为 `accepted` 或 `confirmed`,且 `active_birth_provenance.contract == "chart-choice-v1"`。
- 有 `line_at` 时,区间取所选一侧:`chosen_side == "before"` 为 `window.start_at` 到 `line_at`;`after` 为 `line_at` 到 `window.end_at`。
- 没有 `line_at` 时,取整个 `window`。这对应选盘结果为 `certain`,即窗口内不换盘。
- 前误差、后误差都相对 `active_birth_time` 计算。`active_birth_time` 不在区间内的,按需拍板处理,并在 PROGRESS 里写明。
- `window.label_zh` 写成「按生时纠正选定的区间 HH:MM–HH:MM(前 X 分钟、后 Y 分钟)」。
- 另外把 `line_charts` 和 `both_versions` 原样放进 `window`,例如 `resolved_line_charts` 和 `both_versions_domains`。只记录,不参与证据等级的计算。
2. **旧流程的已采纳区间**:只认 `adopted_credible_range` 非空的记录。授权对 `read_report_candidate_range` 做一个只读的小改动:返回值加 `range_source` 字段,取值为 `adopted_credible_range` 或 `candidate_range`,用一个新迁移文件,不改旧迁移。报告只在 `range_source == "adopted_credible_range"`、档案状态是 `accepted` 或 `confirmed`、并且 `rectification_case_id` 与档案一致时才使用。`candidate_range` 一律不用。如果改这个 RPC 牵涉的范围比预想大,这一项可以先不做,在 PROGRESS 写明,直接走第 3 项。
3. **填写时的范围**:`user_stated`、`shichen`、`period`,与 #224 相同。
4. **`default_unknown_30`**。
**哪些旧记录算已采纳:**只有上面第 1、2 项的情况。其余一律走填写范围或默认值,不猜。
## 验收补充
- 合成档案:`accepted` + `chart-choice-v1`,`window` 04:41–05:19,`line_at` 04:59:42,`chosen_side = "after"`,`active_birth_time` 05:00。期望结果:`source = rectified_range`,前约 0.3 分钟、后 19 分钟,`label_zh` 的内容与之对应。
- 同一个档案,若同时有 `declared_window_*`,以选盘页的采纳为准。
- `reported` 状态,或 `provenance` 不是 `chart-choice-v1` 时,走第 2、3、4 项。
- 原任务书里的 `label_zh` / `summary_zh` 和合同两句话,照原样要求。
@@ -0,0 +1,17 @@
# #227 报告出生时间范围诊断
- BUG-1865。执行 Codex,任务书来自 Claude。基线 `65fab6ebdfd6bc5c22324c6857af562097a99d3e`。
- 状态:需拍板。命中任务书中「采纳了生时纠正」与「生时纠正还在进行中」无法从候选范围返回值区分的条件。
## 证据
- `20260720030000_conversational_rectification_transitions.sql` 保存候选范围的起止。`20260722190000_refund_unconfirmed_rectification.sql` 的 `complete_conversational_rectification_with_range` 可在候选仍为 `pending_validation` 时把旧流程 case 标记为 `completed`。
- `20260920020000_adopted_birth_date.sql` 的 `read_report_candidate_range` 优先读匹配的旧 case。它接受 `confirmed` 和 `completed`,两者都只返回 `start_time`、`end_time`。新流程已采纳的普通区间也返回相同形状。
- 报告 worker 对本人报告在分钟尚未确认时会请求候选范围。报告在采纳状态下使用 `active_birth_time`,即用户选中并保存的候选时刻。它可以与推断推荐的代表时刻不同。读取接口没有返回本次范围的采纳状态或与当前计算时刻对应的采纳记录,因此仅凭 `candidateRange` 无法保证两者匹配。
- 验证使用本机独立、无外部网络的 PostgreSQL 17 容器。按基线迁移原文加载该 RPC,配最小测试表和合成记录。四项断言通过:旧流程待确认记录返回区间,其 JSON 分别等于旧流程已确认记录和新流程已采纳记录,新流程仅候选就绪返回空。这是 RPC 查询复现,数据库迁移套件和应用端到端验收仍待实现后运行。
## 需拍板
- 建议给报告读取链路提供与当前出生资料关联的采纳凭据。范围有采纳凭据时用于证据判断,其余情形走任务书的填写范围与默认范围。需明确旧流程哪些记录可认作已采纳,并确认是否授权扩展报告读取接口。
- 只用档案状态过滤,改动较小,但仍需核实返回区间确实对应本次采纳,尤其是旧 case 优先和存量记录。需要明确哪些旧记录可以回退到填写范围。
- 本轮提交诊断记录。业务实现、产品合同和现有测试断言保持原状。
@@ -0,0 +1,78 @@
# TASK:报告的出生时间误差优先用生时纠正得到的区间,其次用填写时选的范围,最后才用 ±30 分钟(2026-11-13)
- Gitea:#227
- 分支:`codex/reading-rectified-window-20261113`,基于 staging `65fab6ebdfd6bc5c22324c6857af562097a99d3e`
- Bug 编号:开工时取 `docs/BUG_HISTORY.md` 末尾的下一个空号,在 PROGRESS 里写明。关联 BUG-1864(#224)。
- 验收:Claude(jyotisha-e6)
不得自行写「产品决定/裁定」。任务书没写到的取舍,一律选「需拍板」。
不得修改或删除既有断言来让测试通过。确需修改的,在 PROGRESS 里逐条写明原值、新值和原因。
占星师的引擎和资料(`scripts/jyotish_engine.py`、`scripts/yoga_engine.py`、`references/` 下她仓库已有的文件、`SKILL.md`)一律不改。产品差异只能放进单独的产品文件,并在 `references/product_runtime_contract.md` 公告(项目规则 2026-11-07)。
占星师回复原文:staging 上的 `docs/tasks/ref-astrologer-acceptance-reply-20261112.md`。下文的「她 §N」都指这份文件。
## 背景(产品负责人 2026-11-13 实测)
产品负责人的账号做过生时纠正,也存下了结果。新导出的报告里有三处矛盾:
- 附录「出生资料来源与不确定范围」写着「时间不确定范围(分钟):19」。这个数来自 `provenanceUncertaintyMinutes(row, candidateRange)`,也就是生时纠正采纳后的可信区间。
- 但解读写的是「出生时间由填报提供……报时窗取前后各 30 分钟」,证据等级也是按 ±30 分钟算的。
- 原因:#224 的 `readingBirthWindow`(`frontend/src/lib/personal-report-longform-birth.ts`)只读填写时的字段(`declared_window_*`、`uncertainty_*`、时辰、时段),没有读 `generatePersonalReportLongform` 手里已经拿到的 `candidateRange`(`read_report_candidate_range` 的结果)。这个账号的来源是旧选项 `family_exact`,填写时也没有给范围,所以落到了默认 ±30。生时纠正、填写信息、报告三个模块没有接起来。这是 #224 任务书漏写的,不是执行错。
**产品负责人的要求:**
1. 做过生时纠正的,按生时纠正得到的时间和区间来算。
2. 没做生时纠正的,按填写出生信息时选的误差区间来算。
3. 两者都没有,才用 ±30 分钟。
## 要做的
1. **确定误差区间,按以下优先级取值**,写进 `birth_time_window` 的 `source` 字段:
1. `rectified_range`:`candidateRange` 存在时使用。
- 普通区间:前误差 = 计算时刻 − 起点,后误差 = 终点 − 计算时刻(跨午夜按现有 `uncertaintyFromDeclaredRange` 的规则处理)。
- 有 `candidate_intervals` 时,取第一段起点到最后一段终点。另外把各段原样放进 `intervals` 字段,以后可以逐段检查边界。
- 计算时刻取报告实际使用的时刻(生时纠正采纳后的 `active_birth_time`)。在 PROGRESS 里写明:这个时刻是不是代表时刻,是否一定落在区间内。不在区间内的,选需拍板。
- 区间只有一分钟(起点等于终点)的,维持现状:按已确认到分钟处理,±1 分钟。
2. `user_stated` / `shichen` / `period`:保持 #224 的现有逻辑。
3. `default_unknown_30`。
2. **Python 端**:`report_birth_window` 的合法来源加上 `rectified_range`。`birth_precision.window` 原样保留 `intervals`。
3. **解读能写对来源**:事实包 `birth_precision.window` 加一个中文说明字段 `label_zh`,由代码固定生成,不交给模型拼。例如:
- 「按生时纠正的可信区间 04:51–05:09(前 9 分钟、后 9 分钟)」
- 「按填写时给的范围(前 10 分钟、后 20 分钟)」
- 「没有误差信息,按前后各 30 分钟」
在产品合同 report-reading 块里加一句:写出生时间精度时,照抄 `birth_precision.window.label_zh`,不得改写成别的来源。这一句之外,合同不改。
4. **边界分钟数的方向**:样稿把「向后 147 分钟才换座」读成了「延伸到 147 分钟、整段都不稳」。
- `birth_precision.ascendant_margins` 每张分盘加 `summary_zh`,由代码生成,例如:「出生时间提前 0.3 分钟上升就换座;推后 147 分钟才换座」。
- 有区间时,再加一句结论:「区间内会换座」或「区间内不换座」。
- 合同里同一处加一句:引用分盘边界时照抄 `summary_zh`。
## 不要碰
- `scripts/report_render/evidence_grade.py` 的边界判断逻辑。它只读 `window`,取值来源变了,逻辑本身不用改。
- `frontend/src/lib/personal-report-reading.ts`(#219 在改)。
- 生时纠正本身的计算和存储、聊天证据卡、冻结文件。
## 验收标准(新测试)
1. 采纳区间 04:51–05:09、计算时刻 05:00 → `window` 为前 9、后 9,`source` 为 `rectified_range`。同一账号同时有填写范围(例如前 30、后 30)时,仍以生时纠正为准。
2. 没有采纳区间、有填写范围 → `user_stated`,行为与 #224 相同。
3. 两者都没有 → `default_unknown_30`。
4. 有 `candidate_intervals` → 用第一段起点和最后一段终点,`intervals` 原样保留。
5. `label_zh`、`summary_zh` 的中文与上面的例子一致,前后方向不颠倒。
6. 改动的旧测试按惯例写明原值、新值和原因。#224、#215、#216 的测试全部通过。
## 自测命令
```
python3 -m pytest -q tests/test_report_birth_window_20261113.py tests/test_report_evidence_grade_20261112.py tests/test_reading_facts_v3_20261112.py tests/test_report_reading_facts.py
python3 scripts/run_quality_gate.py --profile quick --skip-frontend-runtime
cd frontend && npx tsx --test tests/personal-report-birth-window-20261113.test.ts tests/personal-report-longform*.test.ts && npx tsc --noEmit -p . && npx eslint .
```
## 何时停下来选「需拍板」
- 生时纠正采纳的计算时刻不在采纳区间内。
- `read_report_candidate_range` 对「采纳了生时纠正」和「生时纠正还在进行中」返回的结构分不开。