docs(tasks): rectification capability briefs vs upstream (explain layer, range reading BUG-568, unknown-time route)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
This commit is contained in:
Jesse_Chen
2026-09-06 15:36:46 +00:00
co-authored by Claude Fable 5.1
parent 211f9bb2ac
commit 43a26a3ccb
4 changed files with 255 additions and 0 deletions
@@ -0,0 +1,95 @@
# 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")
```