43a26a3ccb
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
9.3 KiB
9.3 KiB
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:00–23:59 的 1440 分钟 Case——分钟级流程在 24 小时上既慢又没意义。另一方面,昨天的校准说明引擎在分钟级近乎随机,但上升星座每 2 小时换一次、Dasha 宫位命中在时段级的差异是真实存在的:时段比较恰恰是这套引擎最有把握的用法。
1. 现状实证
| # | 事实 | 位置 |
|---|---|---|
| 1 | intake 选"完全不清楚"后,文案是"已跳过具体出生时间…生时校正以后需要时再做",并提供"我可以选一段时间范围"回退到 period_only |
birth-time-intake.tsx L222–232 |
| 2 | declaredClockRange(source="unknown") 返回 00:00–23: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 五段(04–08 / 08–12 / 12–18 / 18–23 / 23–04) |
declared-birth-window.ts L19 |
2. 决策记录
- 两段式。 Stage 0"时段比较":Case 以
stage="block_scan"打开,候选窗口 00:00–23:59,引擎以 10 分钟步长(144 个候选)只做事件计分,不生成探针、不出候选卡;输出五个时段各自的相对支持(按时段内候选分求和归一)与"哪些事件在拉开差距"。Stage 1:用户在时段卡上选定一段(或系统领先段被用户认可)→ Case 的candidate_range改为该时段(≤6 小时),stage="minute",进入现有分钟流程。 - 时段卡是四选项:A/B/C 取相对支持最高的三段(标出支持度),D "说不好 / 都不像"。选 A/B/C = 进入该段;选 D = 继续采集经历再比一次(同分钟流程的采集轮)。不写账本、不进推断层(时段选择是窗口决策,不是证据)。
- 家人线索先用。 开场先把
birth_time_clue读给模型:"家人记得天快亮 / 吃晚饭时"之类,模型只能把它映射成一个建议时段放在旁白里,不能改窗口;窗口只由用户点选或事件计分改。 - 门槛。 时段比较前至少 3 件带年月经历、2 个领域(复用
MIN_ACCEPTANCE_*);不够就先采集。block_scan阶段acceptance_allowed / selection_allowed恒 false,不得采用任何分钟。 - 引擎契约:请求新增可选
minute_step(1~15,默认 1);calculation_spec.minuteStep随之;minute_step>1时discriminating_event_probes为空、precision_stage为block_scan;指纹包含minute_step(步长不同结果不同)。 - 时段划分沿用
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:00–23: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. 开工前置命令
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)"
附录 · 耗时复现(虚构资料)
# .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")