Files
Jyotisha/docs/tasks/TASK-rectification-unknown-time-20260906.md
T

106 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# TASK · 生时校正"完全不知道出生时间"路线:先比时段,再进分钟(2026-09-06)
- 基线:`origin/staging` @ `b938c76a`(文档头 `211f9bb2`
- 分支:`codex/rectification-unknown-time-20260906`
- 执行方:coding agent;验收:Claude
- 串行:排在 `TASK-rectification-range-reading-20260906.md` 之后(都改 `case-service.ts` / 采用旁白)
- 涉及文件:`scripts/rectification/contracts.py``scripts/rectification/scoring_service.py``calculation_spec`)、`scripts/active_rectification_event_engine.py::_candidate_datetimes``scripts/rectification/api_service.py``frontend/src/lib/rectification-agentic/v9/case-service.ts``engine-client.ts``method-followup.ts``choice-card.ts``frontend/src/app/api/rectification/cases/open/route.ts``frontend/src/components/birth-time-intake.tsx``frontend/src/lib/declared-birth-window.ts`
- 不改:`scripts/jyotish_api_server.py` 主体、`page.tsx`、采用/确认门
- BUG 编号:能力补齐,不预留
## 0. 为什么做这件事
上游 yinduzhanxing 便携 skill 的 `unknown` 路线是:先用高信号领域比 `morning / afternoon / evening / night` 四个时段,赢的时段下一轮再切三段。我们的 intake 有 `birth_time_source=unknown`,但之后要么劝退("生时校正以后需要时再做"),要么按代码路径开一个 00:0023:59 的 1440 分钟 Case——分钟级流程在 24 小时上既慢又没意义。另一方面,昨天的校准说明引擎在**分钟级**近乎随机,但上升星座每 2 小时换一次、Dasha 宫位命中在**时段级**的差异是真实存在的:时段比较恰恰是这套引擎最有把握的用法。
## 1. 现状实证
| # | 事实 | 位置 |
| --- | --- | --- |
| 1 | intake 选"完全不清楚"后,文案是"已跳过具体出生时间…生时校正以后需要时再做",并提供"我可以选一段时间范围"回退到 `period_only` | `birth-time-intake.tsx` L222232 |
| 2 | `declaredClockRange(source="unknown")` 返回 `00:0023:59``deriveCandidateRange` 据此给 Case 一个 1440 分钟窗口;引擎 `_candidate_datetimes` 允许 ≤1440 | `declared-birth-window.ts` L31、L146`case-service.ts` L123`event_engine.py` L96 |
| 3 | 旧 journey 链 `scanInput()` 对 unknown 返回 null(那条链已不是 v9 主链) | `birth-time-journey-assessment.ts` L13 |
| 4 | intake 已收集 `birth_time_clue`(家人线索文本)并落 profile,但校正没有用它 | `birth-time-intake-model.ts` L379`birth-time-journey-dynamic-case.ts` L58 |
| 5 | 引擎耗时实测(虚构资料,3 件事):61 分钟窗口 0.9 s,241 分钟 3.2 s;线性外推 1440 分钟 ≈ 20 s,7 件事约 ×2 | 本单附录脚本 |
| 6 | 时段模型:`DECLARED_PERIOD_RANGES` 五段(0408 / 0812 / 1218 / 1823 / 2304 | `declared-birth-window.ts` L19 |
## 2. 决策记录
1. **两段式。** Stage 0"时段比较"Case 以 `stage="block_scan"` 打开,候选窗口 00:00–23:59,引擎以 **10 分钟步长**(144 个候选)只做事件计分,不生成探针、不出候选卡;输出五个时段各自的相对支持(按时段内候选分求和归一)与"哪些事件在拉开差距"。Stage 1:用户在时段卡上选定一段(或系统领先段被用户认可)→ Case 的 `candidate_range` 改为该时段(≤6 小时),`stage="minute"`,进入现有分钟流程。
2. **时段卡是四选项**:A/B/C 取相对支持最高的三段(标出支持度),D "说不好 / 都不像"。选 A/B/C = 进入该段;选 D = 继续采集经历再比一次(同分钟流程的采集轮)。不写账本、不进推断层(时段选择是窗口决策,不是证据)。
3. **家人线索先用。** 开场先把 `birth_time_clue` 读给模型:"家人记得天快亮 / 吃晚饭时"之类,模型只能把它映射成一个**建议时段**放在旁白里,不能改窗口;窗口只由用户点选或事件计分改。
4. **门槛。** 时段比较前至少 3 件带年月经历、2 个领域(复用 `MIN_ACCEPTANCE_*`);不够就先采集。`block_scan` 阶段 `acceptance_allowed / selection_allowed` 恒 false,不得采用任何分钟。
5. **引擎契约**:请求新增可选 `minute_step`115,默认 1);`calculation_spec.minuteStep` 随之;`minute_step>1``discriminating_event_probes` 为空、`precision_stage``block_scan`;指纹包含 `minute_step`(步长不同结果不同)。
6. 时段划分沿用 `DECLARED_PERIOD_RANGES` 五段,不引入上游的 dawn/noon 七段。
## 3. 硬红线
- `block_scan` 阶段不得出现候选卡、采用按钮、代表分钟;旁白不得说"更像 X 点"。
- 分钟流程(stage=minute)的全部既有用例原样通过;`minute_step=1` 时引擎输出与改前逐字节一致(`test_rectification_v5_services.py` 已有指纹用例加一条)。
- 24 小时 × 1 分钟不再允许开 Case(`deriveCandidateRange` 对 unknown 只走 block_scan)。
- 引擎耗时:`block_scan` 单次 ≤ 15 s(7 件事),超出要在进度记录里写实测并给出步长/事件上限方案。
- 真实用户资料不进测试。
## 4. 任务分解
### C1 引擎步长
- `contracts.py` 接收 `minute_step``_candidate_datetimes` 用它;`calculation_spec``minuteStep``scoring_service.build_event_contribution_matrix` 的网格一致性检查沿用;`api_service.score_candidates``minute_step>1` 时跳过 `build_refinement_packet` 的探针部分,`precision_stage={"current":"block_scan"}`。新增 `api_service.block_scan(request)`:返回 `blocks: [{period, start_time, end_time, relative_support, top_events[]}]`(按五段聚合)。`jyotish_api_server.py` 只加一行注册。
- 验收:`tests/test_rectification_v5_services.py`——`minute_step=1` 指纹/输出不变;`minute_step=10` 候选数 = 144`block_scan` 五段支持度和为 100;耗时断言(≤15 s,7 件事虚构资料)。
### C2 Case 阶段
- `agentic_rectification_cases``stage text default 'minute'`(迁移,需 `test:db`);`case-service.ts::deriveCandidateRange``unknown` 返回 00:0023:59 且 `stage=block_scan``open` 路由透传。
- 验收:`rectification-v9-database.test.ts`Docker)或 `BLOCKED.md``case-service` 单测:unknown → block_scan。
### C3 采集与时段卡
- `method-followup.ts``stage=block_scan` 时不走 `rankRenderableDiscriminators`,只走采集链;训练门开后由 `decideRectification` 新分支 `ask_block_choice`(不复用 `discriminate`);`choice-card.ts``choice_kind: "block_choice"`(TS 内部,不进 Python 探针契约);答题写 `case.candidate_range` + `stage=minute`,并触发一次正常重算。
- `engine-client.ts` 新增 `runV9BlockScan``rectification-v9-tools.ts` 的 compare 在 block_scan 阶段调它。
- 开场:`openingBrief` 加入 `birth_time_clue`(决策 3);intake 的 unknown 文案改为"可以直接开始生时校正:先从你记得的经历比出大致时段",删掉劝退句。
- 验收:`rectification-block-scan-20260906.test.ts`——3 件事 2 域后 next_action 为 `ask_block_choice`;选 B 后 candidate_range = 该段、stage=minute、`snapshotCurrent=false`;选 D 后回到采集;block_scan 阶段 `can_adopt=false`、无候选卡。
### C4 记录
- `CHANGELOG.md``PROGRESS-rectification-unknown-time-20260906.md``docs/testing/rectification-unknown-time-20260906.md`(真实环境:intake 选"完全不清楚"→ 开校正 → 说 3 件事 → 出时段卡 → 选一段 → 进入分钟流程);`frontend/DESIGN.md``frontend/docs/VOICE.md`SKILL.md 若需要新增 `block_scan` 状态一行则 bump 到 10.0.15 并在 CHANGELOG 写明。
## 5. 让步顺序
C1 + C2 + C3 不可拆(缺一个就没有产品价值);C4 不可省。若耗时红线过不了,先把步长提到 15 分钟并写明。
## 6. 开工前置命令
```bash
git fetch origin --prune
git worktree add -b codex/rectification-unknown-time-20260906 .worktrees/rectification-unknown-time-20260906 origin/staging
cd .worktrees/rectification-unknown-time-20260906
ln -s /workspace/Jyotisha/frontend/node_modules frontend/node_modules
ln -s /workspace/Jyotisha/.venv .venv
.venv/bin/python -m pytest tests/test_rectification_v5_services.py tests/test_active_rectification_api.py -q
cd frontend && ls tests/rectification-*.test.ts | grep -v database | xargs npx tsx --test 2>&1 | grep -E "^# (tests|pass|fail)"
```
## 附录 · 耗时复现(虚构资料)
```python
# .venv/bin/python,在仓库根目录
import time, uuid, sys
sys.path.insert(0, "."); sys.path.insert(0, "scripts")
from scripts.rectification.contracts import normalize_rectification_request
from scripts.rectification.api_service import score_candidates
EV = [("education","education_start","2016-09-01","2016-09-30","month"),
("relationship","relationship_end","2024-08-08","2024-08-08","day"),
("relocation","relocation","2023-07-01","2023-07-31","month")]
for start, end in (("04:30","05:30"), ("13:00","17:00")):
req = normalize_rectification_request({"birth_date":"1998-03-15","start_time":start,"end_time":end,
"lat":39.9042,"lon":116.4074,"tz":8.0,"events":[{"id":str(uuid.uuid4()),"domain":d,"event_kind":k,
"date_start":s,"date_end":e,"precision":p,"summary":k} for d,k,s,e,p in EV]})
t = time.time(); out = score_candidates(req)
print(start, end, len(out["candidate_scores"]), "minutes", round(time.time()-t, 1), "s")
```
## 验收(Claude2026-09-07`origin/staging` @ `814c924e`
| 项 | 结论 |
| --- | --- |
| C1 引擎 `minute_step` | 通过。我用任务书附录的虚构资料在新旧引擎各跑一次:`candidate_scores`、公开候选与支持度、探针键、`result_id``calculation_spec_hash` 全部相等,`minute_step=1` 字节级不变成立。`minute_step>1` 不出探针、`precision_stage=block_scan` |
| C1 `block_scan` 聚合 | **未通过(P1BUG-570**:五段支持度 = 各段内候选**原始分之和**。10 分钟步长下五段候选数是 24 / 24 / 36 / 30 / 30,下午段仅因为长 6 小时就先天占 25%,清晨 16.7%;引擎原始分底座又远大于事件差(BUG-560),于是首张时段卡基本是在比时段长度。应按段内均值并减去全日最低分归一,等分网格必须得到 20/20/20/20/20 |
| C2 Case 阶段 | 通过(代码审阅)。`stage` 列 + check 约束 + 三个 RPC;存量 unknown 全日 Case 回填为 block_scan;无 Docker`test:db` 37/0 取执行方数字 |
| C3 采集与时段卡 | 通过。门槛用 3 件 / 2 域(不走 holdout 分层,执行方理由成立);A/B/C 改窗口切 minute 并重算;D 只写 `declined_at_fingerprint`block_scan 阶段 `can_adopt`/`selection_allowed` 恒 false、`latestProjection` 为空;开场读 `birth_time_clue` 只进旁白 |
| C4 记录 | 通过;SKILL 不 bump 的理由(stage 非 Case status)成立 |