Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7721aa9a2c | ||
|
|
8e932959b5 | ||
|
|
f80f3b75f8 | ||
|
|
cbe665107f | ||
|
|
8015e8efb7 | ||
|
|
5c5f333d4d | ||
|
|
2243220841 |
@@ -1,5 +1,25 @@
|
||||
# 印度占星 Skill 更新日志
|
||||
|
||||
## 2026-10-28 — 生时校正对话说得更清楚
|
||||
|
||||
- 开场那句会说明是按什么时间开始找的(出生记录、你填的大概时间,或你给的时间段)。
|
||||
- 你说的一件事如果在现在的范围里哪个时刻都对得上、分不出前后,会直接说一句,不再什么都不提。
|
||||
- 打分和出结果的规则不变。Skill 版本不变(issue #148)。
|
||||
|
||||
## 2026-10-28 — 生时校正的结果卡和盘面
|
||||
|
||||
- 结果分得出先后时,卡片最上面直接写最可能的出生时间和上升,按钮「采用 这个时间 并打开星盘」,采用后直接去星盘页;还不能排除的时间留在下面,仍然可以改用。
|
||||
- 卡上可以点「我说错了,改一条」:选一条记下的事,输入框里先写好开头,补完发出就会改掉那一条。
|
||||
- 代表时刻只在对话上方的时间轴写一次;当前盘面改成点「看盘面」再打开。
|
||||
- Skill 版本不变(issue #146、#147)。
|
||||
|
||||
## 2026-10-28 — 两处分盘敏感点按来源修正
|
||||
|
||||
- Sarpa Drekkana 从三段补成书上的五段:巨蟹第 3 段、天蝎第 2 段以前漏掉,现在会标出来;正好落在 10 度边界上的盘不再划到上一段。
|
||||
- 第 22 号 Drekkana 改为 D3 的第 8 宫(以前多移了 14 个星座)。这个点目前还没接进完整解读的敏感点表,所以报告上看不到差别。
|
||||
- 生时校正 20 条密封案例与 900 个报时窗口,名次与分钟误差与上一份回放逐条相同。
|
||||
- Skill 版本不变(BUG-1494、BUG-1495,issue #159)。
|
||||
|
||||
## 2026-10-28 — 生时校正有了自己的入口
|
||||
|
||||
- 侧栏「星盘」下面新增「生时校正」,打开一页就能看到现在的状态:没做过就开始,没做完就继续,做完了看结果或再校正一次。开始前先核对一遍出生资料。
|
||||
|
||||
@@ -20416,3 +20416,34 @@
|
||||
- 关联:ERR-111(特征指纹随哈希种子变)、BUG-1482(#132 让模块能按名字被子进程找到)。
|
||||
- 复发自:无
|
||||
- 修复版本:分支 `codex/heavy-compute-processes-20261028`。
|
||||
|
||||
|
||||
## BUG-1494 | Sarpa Drekkana 少两段,边界还按四舍五入后的度数判段
|
||||
|
||||
- 状态:resolved(代码 + 回归测试;待部署)
|
||||
- 首次发现 / 最近更新:2026-10-28 / 2026-10-28(issue #159,上游 `50847e57`)
|
||||
- 影响面:完整解读里的 Sarpa Drekkana 标记。`scripts/varga.py`。
|
||||
- 现象:只认 Cancer-2、Scorpio-1、Pisces-3 三段。Raman《Hindu Predictive Astrology》Drekkana 章是五段,Cancer-3 与 Scorpio-2 漏掉。判段还先把星座内度数四舍五入到 4 位再整除,边界附近会划到上一段。
|
||||
- 触发条件:行星或上升落在被漏掉的两段,或经度正好在 10 度边界附近(如 100.0、220.0、360.0)。
|
||||
- 根因:`SARPA_DREKKANA_SIGNS` 一个星座只记一个段号;`int(round(deg,4)/10)+1` 让 99.99999 与 100.0 之类的边界口径不稳。
|
||||
- 修复:每星座改成段号元组(Cancer 2/3、Scorpio 1/2、Pisces 3),用原始经度 `int((lon % 30) / 10) + 1` 判段,`definition` 改成五段文字。生时校正不读这两个函数。
|
||||
- 验证:`tests/test_upstream_sync9b_sarpa_precision.py`(6 条边界参数化)、`tests/test_upstream_sync9b_varga_bphs.py`(36 段全枚举与五段区间断言)、`tests/test_varga_bphs.py` 通过。密封校正 20 条与报时窗口 900 条与上一份记录逐条相同。
|
||||
- 防复发:36 段全枚举 + 区间边界测试;判段只用原始经度,不再用显示用的四舍五入度数。
|
||||
- 关联:BUG-1495。
|
||||
- 复发自:无。
|
||||
- 修复版本:分支 `codex/upstream-sync9b-20261028`。
|
||||
|
||||
## BUG-1495 | 第 22 号 Drekkana 按 21 个十度分段取,多移了 14 个星座
|
||||
|
||||
- 状态:resolved(代码 + 回归测试;待部署)
|
||||
- 首次发现 / 最近更新:2026-10-28 / 2026-10-28(issue #159,上游 `50847e57`)
|
||||
- 影响面:`scripts/varga.py::calc_22nd_drekkana`。全仓只有单元测试直接调用它;完整解读的敏感点表目前没有生产者写 `drekkana_22nd`(`_calc_sensitive_points` 只产 bhrigu_bindu 与 sarpa_drekkana),所以这一项目前不会出现在报告里。这是既有接线缺口,本单不接线,也不把「注册了」写成「已计算」。
|
||||
- 现象:结果取 D3 星座 +21。《Crux of Vedic Astrology》p.329:第 22 号 Drekkana 是 D3 的第 8 宫,即 +7。
|
||||
- 触发条件:调用 `calc_22nd_drekkana`。
|
||||
- 根因:把「在黄道上数 21 个十度分段」当成「在 D3 里移 21 个星座」。
|
||||
- 修复:改 `+7`;`offset_from_lagna_drekkana` 仍是 22,输出字段名不变。
|
||||
- 验证:`tests/test_upstream_sync9b_varga_bphs.py` 的固定反例(射手 D3 起,第 8 宫为巨蟹)与 36 段独立枚举(不调用生产偏移)通过;`tests/test_varga_bphs.py` 同步改成固定反例。密封 20 条 + 报时 900 条与上一份记录逐条相同。
|
||||
- 防复发:独立枚举测试用「数满 22 个十度分段再映射 D3」的旁路口径,不复用生产公式。
|
||||
- 关联:BUG-1494。
|
||||
- 复发自:无。
|
||||
- 修复版本:分支 `codex/upstream-sync9b-20261028`。
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
{
|
||||
"record_version": "reported-offset-native-candidate-date-v3",
|
||||
"frozen_at_utc": "2026-10-28T08:19:09.924155+00:00",
|
||||
"ayanamsa": "raman",
|
||||
"node_mode": "mean",
|
||||
"radii_minutes": [
|
||||
15,
|
||||
30,
|
||||
60
|
||||
],
|
||||
"offsets_minutes": [
|
||||
-30,
|
||||
-20,
|
||||
-15,
|
||||
-10,
|
||||
-8,
|
||||
-5,
|
||||
-3,
|
||||
0,
|
||||
3,
|
||||
5,
|
||||
8,
|
||||
10,
|
||||
15,
|
||||
20,
|
||||
30
|
||||
],
|
||||
"minute_step": 1,
|
||||
"dataset": "references/real_case_calibration/minute_rectification_holdout_v3.json",
|
||||
"dataset_sha256": "45aa76ab79dfc6c6404b6252083e1aa5c7e2e2b7de811fc11042a8ad2da3f0ea",
|
||||
"dataset_benchmark_id": "minute_rectification_fact_ranker_v4_holdout_v3",
|
||||
"implementation_sha256": "b44505aa0034e486fcfc29c8611a9559333a97c1005f92e6a5356bd855122205",
|
||||
"implementation_sha256_prefix": "b44505aa0034e486",
|
||||
"historical_artifacts_manifest_sha256": "6102a26a840be207b5858b3a4c0509274468ae9071e86d308cbbac7371d6864e",
|
||||
"production_scoring_files": [
|
||||
"scripts/active_rectification_event_engine.py",
|
||||
"scripts/active_rectification_events.py",
|
||||
"scripts/ashtakavarga.py",
|
||||
"scripts/dasha_analyzer.py",
|
||||
"scripts/divisional_charts_extended.py",
|
||||
"scripts/domain_calculation_service.py",
|
||||
"scripts/functional_benefics.py",
|
||||
"scripts/jaimini.py",
|
||||
"scripts/minute_rectification_fact_ranker_v4.py",
|
||||
"scripts/minute_rectification_feature_facts_v4.py",
|
||||
"scripts/narayana_dasha.py",
|
||||
"scripts/rectification/api_service.py",
|
||||
"scripts/rectification/candidate_contrast.py",
|
||||
"scripts/rectification/candidate_window.py",
|
||||
"scripts/rectification/case_holdout.py",
|
||||
"scripts/rectification/contracts.py",
|
||||
"scripts/rectification/dasha_transition_proximity.py",
|
||||
"scripts/rectification/decision_policy.py",
|
||||
"scripts/rectification/event_probes.py",
|
||||
"scripts/rectification/refinement_packet.py",
|
||||
"scripts/rectification/scoring_service.py",
|
||||
"scripts/shadbala.py",
|
||||
"scripts/varga.py"
|
||||
],
|
||||
"production_scoring_sha256": "7a727418651321d1b9073588587050c4dc6292587748cf35734dd76cefb0effe",
|
||||
"research_files": [
|
||||
"scripts/research/reported_offset_sweep.py",
|
||||
"scripts/research/cluster_width_lib.py",
|
||||
"scripts/research/probe_supply_after_six.py",
|
||||
"scripts/research/sealed_holdout_rerun.py",
|
||||
"scripts/minute_rectification_blind_eval.py",
|
||||
"scripts/minute_rectification_fact_blind_eval_v4.py",
|
||||
"scripts/minute_rectification_holdout_validator.py"
|
||||
],
|
||||
"research_implementation_sha256": "6fa0b0c335ac0cdc25ee887d432ae2f1f6487c665aed9665ad5cc0bc6ef0283b",
|
||||
"file_sha256": {
|
||||
"scripts/active_rectification_event_engine.py": "96c537d221a84680ddaaba74e3335162c44b5bbd9a60d10e69550a2095910d98",
|
||||
"scripts/active_rectification_events.py": "03e333ccf1103caf685dfe50fe890ab1793e945037085f50ce834a4a3b232222",
|
||||
"scripts/ashtakavarga.py": "c524da3f54b2ed9c080b78b33854854a0ca3d5d79b7e536bb1c3bc2be3e02852",
|
||||
"scripts/dasha_analyzer.py": "3949b96813217fe624899856afec9f0b787c4cb0fea3a9a0d961990dbd11943a",
|
||||
"scripts/divisional_charts_extended.py": "e9820e80f14fddc86ab5495d11455d16c2d3c9502a9a242fcf9efc41186e0d11",
|
||||
"scripts/domain_calculation_service.py": "66923a2bc69daf65c965d2973c4b9234edbec5147909eaf3609b6b377ce3c7ca",
|
||||
"scripts/functional_benefics.py": "cbaac3b8c09b97f8724b5300e9a402cbf77089af6c7db8788550d40fd35ac57b",
|
||||
"scripts/jaimini.py": "6b88658da5404da9b136686d92a04be462d61a39da6e8de4255021e2ed2d1173",
|
||||
"scripts/minute_rectification_blind_eval.py": "07aa30d8bad3278669495d5fea8be937aaf6367c8af980ff4f2fe9aa84ccc6ff",
|
||||
"scripts/minute_rectification_fact_blind_eval_v4.py": "bdf460e3467208e8b87345c5beb2128f3e22de7eeffd68f72b281b3ad9affc92",
|
||||
"scripts/minute_rectification_fact_ranker_v4.py": "f1a0d8d2482cd754d2e7f253a9606b0373519435a2bf58cfc3753e2b9ae0fb76",
|
||||
"scripts/minute_rectification_feature_facts_v4.py": "3d899a2cdd4825b8e32062443d487edcd2173a4ea55a36eb9f3abe587c0faf0f",
|
||||
"scripts/minute_rectification_holdout_validator.py": "b60b1c752e58672beaee8b36eb3b2348169726cda070ec969db0f13e7ed715af",
|
||||
"scripts/narayana_dasha.py": "bc667c30c9562fb73fb0b7b4c22a5e377f9a531795141fc629b2cd3cf396773a",
|
||||
"scripts/rectification/api_service.py": "089730097d3872a4f7b5d7dd5574bb7e9ab4d2e246d4a380800bc824d7979b95",
|
||||
"scripts/rectification/candidate_contrast.py": "eee7da71c0574a08876404b78ca111153fa95fb83ace82b9fdca141d1992e273",
|
||||
"scripts/rectification/candidate_window.py": "2fbc5058b12049b15fca375cdc125b8283e19f9f3e4bedcc291e344501891bef",
|
||||
"scripts/rectification/case_holdout.py": "fe0e699b617d9e216acdfeff87af4221c1d6f4c101bf33e1dbf14e2bcd75a8b5",
|
||||
"scripts/rectification/contracts.py": "c6dc6bd230e38f9f2126c224aae9ddbd6415304c15340f8c7cd3f06f6ef3160f",
|
||||
"scripts/rectification/dasha_transition_proximity.py": "5ce4ed27b0f06c681abc34e5fd998fafb10c153a0070aa36c99ddfa869a2cbf2",
|
||||
"scripts/rectification/decision_policy.py": "80e8de4680d19a052213bb821d2a9c94143a2962235aeb27f6ad81374d2e60d5",
|
||||
"scripts/rectification/event_probes.py": "f5ca9d54501f67a1546fcf5b01da0c1277dec2e2c6df481884e8900d14909a7c",
|
||||
"scripts/rectification/refinement_packet.py": "dbeea5aa780889785ce850f3881e8b7f67bb44594e9bc382c65b2e730884a2f4",
|
||||
"scripts/rectification/scoring_service.py": "fa056fa30d9e6ffa4b75295fc49c793e35b26ebeca64ca843d26c8abef3050f2",
|
||||
"scripts/research/cluster_width_lib.py": "1d8f2ffa7795d62069deebb2de8e16749317037d4ed3f24a2c0092676e5cfcc6",
|
||||
"scripts/research/probe_supply_after_six.py": "098a5b4398d6f8d996917d809c2fc922ad64031d3b70660f8ef7c876aba92438",
|
||||
"scripts/research/reported_offset_sweep.py": "8ecc381dfaa3c0e6bea058cfa44c606aabdb1086c137f16da82ee5a3bf5ccc91",
|
||||
"scripts/research/sealed_holdout_rerun.py": "ebcdae760f2b76c81d5d0c8e2bf347cff4c81cca9074f27ae94b6734408ccd42",
|
||||
"scripts/shadbala.py": "334dc9d8e4292f0c95d40fd4c29285aa89b9e49d4f187fb9498f4bb0ad93bdfc",
|
||||
"scripts/varga.py": "8ff96b9fc8346ca249abff6109434b6ed57fa150e1a9854d4f73bdf317568368"
|
||||
},
|
||||
"hash_scope": "explicit_identity_file_sets_not_a_transitive_dependency_lock",
|
||||
"scorer": "native_event_contribution_matrix_not_shadow_fact_ranker",
|
||||
"delivery": "initial_signature_clusters_peak_gap_lt_8_envelope_no_answers_no_elimination",
|
||||
"rank": "score_desc_then_sha256(benchmark_id:case_id:candidate_time)",
|
||||
"grid": "every_minute_inclusive_not_production_two_minute_sampling",
|
||||
"cross_midnight": "date_aware_candidates_distance_and_native_single_matrix",
|
||||
"evaluator_sha256": "8ecc381dfaa3c0e6bea058cfa44c606aabdb1086c137f16da82ee5a3bf5ccc91",
|
||||
"replay_revision": "native_candidate_date_v3",
|
||||
"supersedes": "candidate_date_grouped_v2_identity_and_path_not_assumed_numerically_wrong",
|
||||
"is_blind_evaluation": false,
|
||||
"truth_hidden_from_ranker": true,
|
||||
"official_valid_independent_blind": false,
|
||||
"official_blind_trial_count": 0,
|
||||
"results_previously_seen": true,
|
||||
"must_not_use_for_tuning": true
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"record_version": "exposed-v3-fixed-protocol-cross-midnight-rerun-v2",
|
||||
"extended_identity": {
|
||||
"historical_artifacts_manifest_sha256": "6102a26a840be207b5858b3a4c0509274468ae9071e86d308cbbac7371d6864e",
|
||||
"production_scoring_files": [
|
||||
"scripts/active_rectification_event_engine.py",
|
||||
"scripts/active_rectification_events.py",
|
||||
"scripts/ashtakavarga.py",
|
||||
"scripts/dasha_analyzer.py",
|
||||
"scripts/divisional_charts_extended.py",
|
||||
"scripts/domain_calculation_service.py",
|
||||
"scripts/functional_benefics.py",
|
||||
"scripts/jaimini.py",
|
||||
"scripts/minute_rectification_fact_ranker_v4.py",
|
||||
"scripts/minute_rectification_feature_facts_v4.py",
|
||||
"scripts/narayana_dasha.py",
|
||||
"scripts/rectification/api_service.py",
|
||||
"scripts/rectification/candidate_contrast.py",
|
||||
"scripts/rectification/candidate_window.py",
|
||||
"scripts/rectification/case_holdout.py",
|
||||
"scripts/rectification/contracts.py",
|
||||
"scripts/rectification/dasha_transition_proximity.py",
|
||||
"scripts/rectification/decision_policy.py",
|
||||
"scripts/rectification/event_probes.py",
|
||||
"scripts/rectification/refinement_packet.py",
|
||||
"scripts/rectification/scoring_service.py",
|
||||
"scripts/shadbala.py",
|
||||
"scripts/varga.py"
|
||||
],
|
||||
"production_scoring_sha256": "7a727418651321d1b9073588587050c4dc6292587748cf35734dd76cefb0effe",
|
||||
"research_files": [
|
||||
"scripts/research/reported_offset_sweep.py",
|
||||
"scripts/research/cluster_width_lib.py",
|
||||
"scripts/research/probe_supply_after_six.py",
|
||||
"scripts/research/sealed_holdout_rerun.py",
|
||||
"scripts/minute_rectification_blind_eval.py",
|
||||
"scripts/minute_rectification_fact_blind_eval_v4.py",
|
||||
"scripts/minute_rectification_holdout_validator.py"
|
||||
],
|
||||
"research_implementation_sha256": "6fa0b0c335ac0cdc25ee887d432ae2f1f6487c665aed9665ad5cc0bc6ef0283b",
|
||||
"file_sha256": {
|
||||
"scripts/active_rectification_event_engine.py": "96c537d221a84680ddaaba74e3335162c44b5bbd9a60d10e69550a2095910d98",
|
||||
"scripts/active_rectification_events.py": "03e333ccf1103caf685dfe50fe890ab1793e945037085f50ce834a4a3b232222",
|
||||
"scripts/ashtakavarga.py": "c524da3f54b2ed9c080b78b33854854a0ca3d5d79b7e536bb1c3bc2be3e02852",
|
||||
"scripts/dasha_analyzer.py": "3949b96813217fe624899856afec9f0b787c4cb0fea3a9a0d961990dbd11943a",
|
||||
"scripts/divisional_charts_extended.py": "e9820e80f14fddc86ab5495d11455d16c2d3c9502a9a242fcf9efc41186e0d11",
|
||||
"scripts/domain_calculation_service.py": "66923a2bc69daf65c965d2973c4b9234edbec5147909eaf3609b6b377ce3c7ca",
|
||||
"scripts/functional_benefics.py": "cbaac3b8c09b97f8724b5300e9a402cbf77089af6c7db8788550d40fd35ac57b",
|
||||
"scripts/jaimini.py": "6b88658da5404da9b136686d92a04be462d61a39da6e8de4255021e2ed2d1173",
|
||||
"scripts/minute_rectification_blind_eval.py": "07aa30d8bad3278669495d5fea8be937aaf6367c8af980ff4f2fe9aa84ccc6ff",
|
||||
"scripts/minute_rectification_fact_blind_eval_v4.py": "bdf460e3467208e8b87345c5beb2128f3e22de7eeffd68f72b281b3ad9affc92",
|
||||
"scripts/minute_rectification_fact_ranker_v4.py": "f1a0d8d2482cd754d2e7f253a9606b0373519435a2bf58cfc3753e2b9ae0fb76",
|
||||
"scripts/minute_rectification_feature_facts_v4.py": "3d899a2cdd4825b8e32062443d487edcd2173a4ea55a36eb9f3abe587c0faf0f",
|
||||
"scripts/minute_rectification_holdout_validator.py": "b60b1c752e58672beaee8b36eb3b2348169726cda070ec969db0f13e7ed715af",
|
||||
"scripts/narayana_dasha.py": "bc667c30c9562fb73fb0b7b4c22a5e377f9a531795141fc629b2cd3cf396773a",
|
||||
"scripts/rectification/api_service.py": "089730097d3872a4f7b5d7dd5574bb7e9ab4d2e246d4a380800bc824d7979b95",
|
||||
"scripts/rectification/candidate_contrast.py": "eee7da71c0574a08876404b78ca111153fa95fb83ace82b9fdca141d1992e273",
|
||||
"scripts/rectification/candidate_window.py": "2fbc5058b12049b15fca375cdc125b8283e19f9f3e4bedcc291e344501891bef",
|
||||
"scripts/rectification/case_holdout.py": "fe0e699b617d9e216acdfeff87af4221c1d6f4c101bf33e1dbf14e2bcd75a8b5",
|
||||
"scripts/rectification/contracts.py": "c6dc6bd230e38f9f2126c224aae9ddbd6415304c15340f8c7cd3f06f6ef3160f",
|
||||
"scripts/rectification/dasha_transition_proximity.py": "5ce4ed27b0f06c681abc34e5fd998fafb10c153a0070aa36c99ddfa869a2cbf2",
|
||||
"scripts/rectification/decision_policy.py": "80e8de4680d19a052213bb821d2a9c94143a2962235aeb27f6ad81374d2e60d5",
|
||||
"scripts/rectification/event_probes.py": "f5ca9d54501f67a1546fcf5b01da0c1277dec2e2c6df481884e8900d14909a7c",
|
||||
"scripts/rectification/refinement_packet.py": "dbeea5aa780889785ce850f3881e8b7f67bb44594e9bc382c65b2e730884a2f4",
|
||||
"scripts/rectification/scoring_service.py": "fa056fa30d9e6ffa4b75295fc49c793e35b26ebeca64ca843d26c8abef3050f2",
|
||||
"scripts/research/cluster_width_lib.py": "1d8f2ffa7795d62069deebb2de8e16749317037d4ed3f24a2c0092676e5cfcc6",
|
||||
"scripts/research/probe_supply_after_six.py": "098a5b4398d6f8d996917d809c2fc922ad64031d3b70660f8ef7c876aba92438",
|
||||
"scripts/research/reported_offset_sweep.py": "8ecc381dfaa3c0e6bea058cfa44c606aabdb1086c137f16da82ee5a3bf5ccc91",
|
||||
"scripts/research/sealed_holdout_rerun.py": "ebcdae760f2b76c81d5d0c8e2bf347cff4c81cca9074f27ae94b6734408ccd42",
|
||||
"scripts/shadbala.py": "334dc9d8e4292f0c95d40fd4c29285aa89b9e49d4f187fb9498f4bb0ad93bdfc",
|
||||
"scripts/varga.py": "8ff96b9fc8346ca249abff6109434b6ed57fa150e1a9854d4f73bdf317568368"
|
||||
},
|
||||
"hash_scope": "explicit_identity_file_sets_not_a_transitive_dependency_lock"
|
||||
},
|
||||
"production_identity_scope": "context_only_shadow_scorer_does_not_call_transition_proximity",
|
||||
"frozen_at_utc": "2026-10-28T08:19:06.008600+00:00",
|
||||
"dataset_path": "references/real_case_calibration/minute_rectification_holdout_v3.json",
|
||||
"dataset_sha256": "45aa76ab79dfc6c6404b6252083e1aa5c7e2e2b7de811fc11042a8ad2da3f0ea",
|
||||
"algorithm_version": "birth-time-event-fact-ranker-v4-shadow",
|
||||
"implementation_sha256": "b44505aa0034e486fcfc29c8611a9559333a97c1005f92e6a5356bd855122205",
|
||||
"files": [
|
||||
"scripts/active_rectification_event_engine.py",
|
||||
"scripts/active_rectification_events.py",
|
||||
"scripts/ashtakavarga.py",
|
||||
"scripts/dasha_analyzer.py",
|
||||
"scripts/divisional_charts_extended.py",
|
||||
"scripts/domain_calculation_service.py",
|
||||
"scripts/jaimini.py",
|
||||
"scripts/minute_rectification_fact_ranker_v4.py",
|
||||
"scripts/minute_rectification_feature_facts_v4.py",
|
||||
"scripts/narayana_dasha.py",
|
||||
"scripts/shadbala.py",
|
||||
"scripts/varga.py"
|
||||
],
|
||||
"historical_frozen_sha256": "f41c298dd6cdcebe7a93e632f7191954be7987f6012ca2a34cecb6e447fbf196",
|
||||
"evaluator_sha256": "ebcdae760f2b76c81d5d0c8e2bf347cff4c81cca9074f27ae94b6734408ccd42",
|
||||
"ayanamsa": "raman",
|
||||
"node_mode": "mean",
|
||||
"candidate_radius_minutes": [
|
||||
10
|
||||
],
|
||||
"minute_step": 1,
|
||||
"release_metrics": {
|
||||
"top_1_rate_minimum": 0.6,
|
||||
"top_3_rate_minimum": 0.85,
|
||||
"mean_absolute_minute_error_maximum": 2.0,
|
||||
"false_confirmation_rate_maximum": 0.05,
|
||||
"correct_insufficient_evidence_rejection_rate_minimum": 0.9
|
||||
},
|
||||
"results_previously_seen": true,
|
||||
"official_valid_independent_blind": false,
|
||||
"must_not_use_for_tuning": true,
|
||||
"tie_breaker": "sha256(benchmark_id:case_id:candidate_time); published minute is never passed to the ranker",
|
||||
"metric_rank_definition": "competition_rank_1_plus_strictly_higher_scores_legacy_protocol",
|
||||
"extra_metric_rank_definition": "score_desc_then_existing_opaque_sha256_total_order"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,45 @@
|
||||
# 22nd Drekkana: division count versus D3 house count
|
||||
|
||||
## Source
|
||||
|
||||
- `references/book_corpus/books/Crux of Vedic Astrology – Timing of Events (Sanjay Rath) (z-library.sk, 1lib.sk, z-lib.sk).md`
|
||||
- Topic: 22nd Drekkana geometry. Corpus page 340, printed page 329.
|
||||
- Lines 13809–13812: `house on the physical body. Incidentally, the 22nd`
|
||||
`decanate is also the 8th house in the Drekkana`
|
||||
`(D-3) chart while the 64th Navamsa is the 4th`
|
||||
`house from the Moon in the Navamsa chart. The`
|
||||
- Located by full-corpus search and read using `search_jyotish_books.py --read-unit 340`.
|
||||
- This review verifies a traditional geometric definition, not health or lifespan prediction.
|
||||
|
||||
## Defect and correction
|
||||
|
||||
The producer added 21 to the D3 sign index. That confuses a count of zodiacal
|
||||
ten-degree divisions with a count of signs in the divisional chart. Use D3
|
||||
ascendant +7 modulo12, the eighth house inclusive.
|
||||
|
||||
The old test repeated the same +21 error. Replace it with a fixed Sagittarius
|
||||
to Cancer counterexample and an independent 36-part zodiac enumeration. Across
|
||||
each part, count 22 inclusively on the original zodiac, then apply the standard
|
||||
Parashari D3 mapping to the resulting part. Test starts, interiors and ends.
|
||||
|
||||
The 64th Navamsa formula is unchanged: D9 Moon +63 modulo12 equals its fourth
|
||||
house, as the same source states. `offset_from_lagna_drekkana=22` is an ordinal
|
||||
label, not the number of D3 signs added.
|
||||
|
||||
No customer information is retained here. Recompute sensitive-point consumers
|
||||
and use a new verified frozen pin before claiming the published report changed.
|
||||
|
||||
## Sarpa classification: missing two divisions
|
||||
|
||||
Raman, `Hindu Predictive Astrology -- Bangalore Venkat Raman, B V Raman -- 1981 -- IBH Prakashana -- isbn13 9788119394883 -- c9102b89e2f8f7c358a913f7d07d7c39 -- Anna’s Archive.md`:
|
||||
|
||||
- Corpus p.279, L10960–10962 explicitly counts five serpent divisions.
|
||||
- Cancer second and third: p.280, L11013–11020.
|
||||
- Scorpio first and second: pp.281–282, L11062–11074.
|
||||
- Pisces third: p.283, L11129–11131.
|
||||
- Search used `search_jyotish_books.py serpent --book <registered filename>`.
|
||||
|
||||
The old map accepted one division per sign and omitted Cancer-3 and Scorpio-2.
|
||||
It now allows multiple divisions per sign. Independent tests cover all 36
|
||||
divisions and their boundaries against the source's three degree intervals.
|
||||
This classifies chart geometry; it does not establish a medical outcome.
|
||||
@@ -0,0 +1,68 @@
|
||||
# PROGRESS · 上游同步第九轮乙批(2026-10-28)
|
||||
|
||||
- 分支:`codex/upstream-sync9b-20261028`;基线 `origin/staging` `12eb7018`
|
||||
- issue:#159(#141 乙批);产品 10-28 选 **A**:允许改冻结文件 `scripts/varga.py`
|
||||
- 上游:`732642856/yinduzhanxing` main `50847e57`(10-16),按语义移植
|
||||
- 结论:B1 两项完成;密封校正 20 条与报时窗口 900 条与上一份记录逐条相同,已按 ERR-110 另存新冻结;B2(Narayana 小运强度口径)按决策记录不做
|
||||
|
||||
## 做了什么
|
||||
|
||||
| 项 | 上游 | BUG | 改了什么 |
|
||||
| --- | --- | --- | --- |
|
||||
| T1 Sarpa 五段 + 原始经度判段 | `50847e57` | BUG-1494 | `SARPA_DREKKANA_SIGNS` 由单段号改段号元组;`calc_sarpa_drekkana` 去掉四舍五入、先取模;`definition` 改五段 |
|
||||
| T2 第 22 号 Drekkana 改 D3 第 8 宫 | `50847e57` | BUG-1495 | `calc_22nd_drekkana` 由 `+21` 改 `+7`,字段名与序号 22 不变 |
|
||||
| T4 上游配套文档 | `50847e57` | — | 原路径放入 `docs/research/sensitive_point_drekkana_count_review_2026_10_14.md` |
|
||||
| T3 重新冻结 | — | — | 两份新冻结记录 + 两份回放报告(ERR-110),旧记录逐字节保留 |
|
||||
|
||||
生产文件只有 `scripts/varga.py` 一个;`git diff --stat` 见下。
|
||||
|
||||
## 与任务书的偏离
|
||||
|
||||
1. **红线 2 的「先不带 `--freeze` 跑」做不到**:`scripts/varga.py` 在冻结记录的扩展身份里,拿 sync8 的冻结记录回放会在打分前被身份闸门拦下(`frozen_record_mismatch:extended_identity`)。改成 ERR-110 的做法:仓库内建临时冻结记录 → 回放 → 与 sync8 逐条比 → 全部相同后才写正式记录。临时记录 `docs/research/.tmp_sweep_sync9b.freeze.json` 用完已删除,未提交。
|
||||
2. **报时窗口脚本必须用仓库内路径**:报告里写 `freeze_record_path`(`relative_to(ROOT)`),所以正式记录直接放 `docs/research/`;比较轮的临时记录也在仓库内。
|
||||
3. **改了前端一个测试文件**:`frontend/tests/rectification-confirmation-gate.test.ts` 写死当前密封报告路径,合同指针一挪就会红。只改这个路径与三栏注释,不动任何产品代码;与第八轮同做法的先例一致。
|
||||
|
||||
## 跑过的命令
|
||||
|
||||
| 检查 | 命令 | 结果 |
|
||||
| --- | --- | --- |
|
||||
| 上游两个测试文件 | `.venv\Scripts\python -m pytest tests/test_upstream_sync9b_varga_bphs.py tests/test_upstream_sync9b_sarpa_precision.py -o addopts="" -q` | 23 passed |
|
||||
| 既有分盘测试一起 | 上一行再加 `tests/test_varga_bphs.py` | 39 passed |
|
||||
| CLI 敏感点(任务书写 `-k "sarpa or drekkana or special"`) | `.venv\Scripts\python -m pytest tests/test_cli_smoke.py -k "sarpa or drekkana or special"` | 0 条被选中(35 deselected,退出码 5);`-k` 只匹配测试名,改用实际测试名 `-k sensitive_point_modules`:1 passed、34 deselected |
|
||||
| 分盘 + CLI 敏感点合并 | 4 个文件/节点一起跑 | 40 passed |
|
||||
| 密封校正 20 条 | `scripts/research/sealed_holdout_rerun.py`(新冻结记录) | `trial_count` 20;8 个字段(`true_rank`、`opaque_true_rank`、`minute_error`、`would_confirm`、`false_confirmation`、`insufficient_evidence_rejected`、`candidate_count`、`event_count`)不一致 0;`metrics` 相同;`changed_trial_count` 仍是 8 |
|
||||
| 报时窗口 900 条 | `scripts/research/reported_offset_sweep.py --json`(新冻结记录) | `trial_count` 900;trial 全字段不一致 0;`summary` 与 `widening_geometry` 相同;`changed_trial_count` 仍是 581 |
|
||||
| 冻结契约 | `tests/test_sealed_holdout_contract_freshness.py` | 9 passed |
|
||||
| 确认门合同 | `tests/test_rectification_confirmation_and.py` | 10 passed |
|
||||
| 其余分盘测试 | `pytest tests/test_varga*.py` | 53 passed、9 failed;9 条全在 `test_varga_vedastro_mode.py`(D2 起 Ascendant 星座不符)。在基线 `12eb7018` 的一次性工作树上同样 9 failed,与 Sarpa / 22nd 无关,不算本单 |
|
||||
| 正式记录生成 | `scripts/research/*.py --freeze` 两份 + 回放两份 | `sealed_holdout_rerun_upstream_sync9b_2026_10_28.{freeze.json,json}`、`reported_offset_upstream_sync9b_2026_10_28.{freeze.json,json}` |
|
||||
|
||||
Python 快速门、前端 tsc / lint / build、`npm test`、`test:db` 没跑:没有改前端产品代码,没有动表。
|
||||
|
||||
## 断言三栏
|
||||
|
||||
| 位置 | 原值 | 新值 | 原因 |
|
||||
| --- | --- | --- | --- |
|
||||
| `tests/test_varga_bphs.py::test_22nd_drekkana_counts_forward_from_lagna_drekkana` | `expected = (drekkana_ref(asc_lon) + 21) % 12` | 固定 `3`(巨蟹),保留 `offset_from_lagna_drekkana == 22` | 旧断言把「+21」的错口径抄进了测试;换成书上的固定反例(射手 D3 起,第 8 宫为巨蟹) |
|
||||
| `tests/test_cli_smoke.py::test_full_reading_exposes_sensitive_point_modules` | `definition == "Cancer-2, Scorpio-1, Pisces-3"` | `definition == "Cancer-2, Cancer-3, Scorpio-1, Scorpio-2, Pisces-3"` | BUG-1494 改的就是这个定义字符串;仍逐字比对,没有放宽 |
|
||||
| `tests/test_sealed_holdout_contract_freshness.py` 排除前缀 | 排除到 `upstream_sync8_2026_10_18` | 新增 `upstream_sync9b_2026_10_28` 两条,原有条目全部保留 | ERR-110 另存新冻结,旧记录留在清单外;第 1 次打回时补回被误删的 sync8 两条 |
|
||||
| `frontend/tests/rectification-confirmation-gate.test.ts` 读取与断言路径 | `…upstream_sync8_2026_10_18.json` | `…upstream_sync9b_2026_10_28.json` | 合同当前指针移到新报告;注释补第 11 条三栏 |
|
||||
| `scripts/research/sealed_holdout_rerun.py`、`reported_offset_sweep.py` 的 `FREEZE` / `REPORT` | `…upstream_sync8_2026_10_18` | `…upstream_sync9b_2026_10_28` | 同上,ERR-110 |
|
||||
|
||||
## 发现但没改
|
||||
|
||||
- 完整解读的敏感点表没有 `drekkana_22nd` 的生产者:`_calc_sensitive_points` 只产 `bhrigu_bindu` 与 `sarpa_drekkana`。所以第 22 号 Drekkana 修好后,报告里暂时看不到变化;这是既有接线缺口,本单不接线(也不把「有函数」写成「报告里已计算」)。
|
||||
- `frontend/tests/fixtures/report-density-fictional-engine.json` 里还有两处旧 `definition` 字符串。它是前端 fixture,本单不改前端产品代码,留给前端轮次或验收方判断。
|
||||
|
||||
## 环境缺口
|
||||
|
||||
- Windows 跑 Python 测试要 `PYTHONUTF8=1`,否则子进程按 GBK 编码,遇到警告符号会 `UnicodeEncodeError`,看起来像引擎崩了。
|
||||
- 前端测试没跑(任务书不要求,且本单没改前端产品代码)。
|
||||
|
||||
## 第 1 次打回(2026-10-28)
|
||||
|
||||
- 被验 SHA:`5c5f333d4d93fb691c10790988d3d4c986ac1bd3`;修复单 `docs/tasks/TASK-upstream-sync9b-fix-20261028.md`
|
||||
- 错在哪:改 `IDENTITY_FORBIDDEN_PREFIXES` 时把第八轮的两条**替换**掉了,清单从 4 条变成 2 条;测试照样通过,但守门变弱(第八轮的冻结记录混进打分身份也不会再被发现)
|
||||
- 改了什么:把第八轮两条加回原来的注释下面,第九轮乙批两条保留 —— 现在清单同时含 sync8 与 sync9b 共 4 条;本文件「断言三栏」对应行改成「新增两条、原有全部保留」
|
||||
- 跑过:`tests/test_sealed_holdout_contract_freshness.py` 9 passed;`frontend/tests/rectification-confirmation-gate.test.ts` 8 passed(`node node_modules/tsx/dist/cli.mjs --test tests/rectification-confirmation-gate.test.ts`,工作树里 `frontend/node_modules` 已存在,没有重跑 `npm ci`)
|
||||
- 没有重跑上游两个测试文件和校正 20 / 900:本次不改计算代码
|
||||
@@ -544,3 +544,4 @@
|
||||
| — | `PROGRESS-session-shell-116-20261028.md` | **#116 侧栏会话菜单搬进外壳**:每个页面都能「…」和拖动(直接执行,BUG-1467) | 已推 staging | `codex/session-shell-116-20261027` |
|
||||
| `TASK-home-crash-saved-people-20261027.md` | `PROGRESS-home-crash-saved-people-20261027.md` | **#115 首页崩溃**:存过别人的账户一进首页就「页面出错了」——人物列表的资料缺两个字段,启程引导一读就报错;入口补齐默认值(BUG-1464) | 待验收 | `codex/home-crash-saved-people-20261027` |
|
||||
| `TASK-upstream-sync9-20261028.md` | `PROGRESS-upstream-sync9-20261028.md` | **第 1 次打回(快速门 1 败:Darakaraka 旧测试锁错值,见 `TASK-upstream-sync9-fix-20261028.md`)** · **#141 上游同步第九轮**:甲批九项已做完并推 `codex/upstream-sync9-20261028`;校正 20 条和 900 条与上一份回放逐条相同;乙批仍等 #137 | 待验收 | `codex/upstream-sync9-20261028` |
|
||||
| `TASK-upstream-sync9b-20261028.md` | `PROGRESS-upstream-sync9b-20261028.md` | **第 1 次打回(冻结契约排除清单删了第八轮两条,见 `TASK-upstream-sync9b-fix-20261028.md`)** · **#159 上游同步第九轮乙批(产品选 A)**:Sarpa Drekkana 补成五段、第 22 号 Drekkana 改为 D3 第 8 宫;改冻结文件 `varga.py`,校正 20 + 900 与上一份逐条相同后按 ERR-110 重新冻结。Narayana 小运强度口径(B2)不做,另问产品 | 待验收 | `codex/upstream-sync9b-20261028` |
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
# TASK · 上游同步第九轮乙批:Sarpa Drekkana 与第 22 号 Drekkana(2026-10-28)
|
||||
|
||||
- 日期:2026-10-28
|
||||
- 来源:issue #159(#141 乙批)。产品 10-28 选 **A**:改冻结文件,生时校正 20 条 + 900 条分数逐条相同才重新冻结。
|
||||
- 执行方:**Grok(Windows + Docker)**,分支 `codex/upstream-sync9b-20261028`,只推这个分支(本任务书就是该分支第一个提交)
|
||||
- 验收:Claude(Linux)
|
||||
- BUG 编号:开工时按 `docs/BUG_HISTORY.md` 全文件取最大号 +1(写作时 staging 最大号 BUG-1492;另一会话仍在占号,开工当时再核对)。本单预计 2 个号。
|
||||
|
||||
## 1. 基线
|
||||
|
||||
- 基线 commit:`origin/staging` `12eb7018`(#141 甲批与 #137 都已在其中)。
|
||||
- 上游:`732642856/yinduzhanxing` main 的 `50847e57`(10-16)。按语义移植,不整文件覆盖。
|
||||
|
||||
## 2. 事故实证(上游测试放到 `12eb7018` 上跑)
|
||||
|
||||
| 项 | 上游测试结果 | 我方现状 | 来源 |
|
||||
| --- | --- | --- | --- |
|
||||
| B1a Sarpa Drekkana | `test_varga_bphs.py` 3 败 14 过;`test_sarpa_drekkana_precision.py` 4 败 2 过 | `scripts/varga.py::SARPA_DREKKANA_SIGNS` 只有 Cancer-2、Scorpio-1、Pisces-3 三段;`calc_sarpa_drekkana` 先把度数四舍五入到 4 位再算第几个 Drekkana,边界附近会划错 | Raman《Hindu Predictive Astrology》Drekkana 章:五段(Cancer-2、Cancer-3、Scorpio-1、Scorpio-2、Pisces-3) |
|
||||
| B1b 第 22 号 Drekkana | 同上 | `scripts/varga.py::calc_22nd_drekkana` 取 D3 星座 +21 | 《Crux of Vedic Astrology》p.329:是 D3 的第 8 宫,即 +7。数 21 个十度分段 ≠ 在 D3 里移 21 个星座 |
|
||||
|
||||
下游:`jyotish_engine.py` 完整解读的特殊点(`calc_sarpa_drekkana`)→ 前端报告事实表 `report-fact-tables.ts` 的 Sarpa Drekkana 行;第 22 号 Drekkana 进完整解读的敏感点。
|
||||
|
||||
## 3. 根因
|
||||
|
||||
前八轮同步与 #141 甲批都没有取上游 main 10-16 的这一提交(#141 甲批因冻结文件把它留到乙批)。
|
||||
|
||||
## 4. 决策记录
|
||||
|
||||
1. **产品 10-28 在 #159 选 A**:允许改冻结文件 `scripts/varga.py`;改完跑生时校正 20 条 + 900 条,**逐条相同**才重新冻结为 `upstream_sync9b_2026_10_28`,沿用第八轮 `79f6ac52` 的做法。有任何一条不同:停,不重新冻结,回报「需拍板」。
|
||||
2. **B2(Narayana 小运强度口径)本单不做。** 规划时核实:上游 `6859b263` 只是把已选的 `strength_profile` 往下传,但这个强度口径家族(`rath_*`、`pvr_*` 等)来自上游 09-27 的 `5b57949c`,我方从未移植;默认口径下结果不变。是否移植整套口径另由产品决定(已在 #159 说明)。
|
||||
3. 用户可见变化已知并接受:部分盘 Sarpa Drekkana 标记会多出来(Cancer 第 3、Scorpio 第 2 段);第 22 号 Drekkana 的星座会变。
|
||||
|
||||
## 5. 硬红线
|
||||
|
||||
1. 只改 `scripts/varga.py` 里 `SARPA_DREKKANA_SIGNS`、`calc_sarpa_drekkana`、`calc_22nd_drekkana` 三处;`varga.py` 其他函数逐字节不变(PROGRESS 贴 `git diff --stat` 与函数级 diff 说明)。
|
||||
2. 生时校正 20 条(`scripts/research/sealed_holdout_rerun.py`)+ 900 条(`scripts/research/reported_offset_sweep.py --json`):先不带 `--freeze` 跑出结果、与 `docs/research/*upstream_sync8_2026_10_18.json` 逐条比;**全部相同**后才按脚本自带的 `--freeze` 写新冻结记录(不覆盖旧记录)。
|
||||
3. 不改其他冻结文件;不改 `.gitea/workflows/**`、数据库、前端。
|
||||
4. 上游断言数值不得放宽;接口名不同只改调用方式。
|
||||
5. 公开仓库:测试只用公开或虚构出生资料。
|
||||
|
||||
## 6. 任务分解
|
||||
|
||||
| 任务 | 内容 | 验收标准 |
|
||||
| --- | --- | --- |
|
||||
| T1 | Sarpa 五段 + 不经四舍五入判段(`50847e57`) | 上游两个测试文件中 Sarpa 相关全过;`definition` 字段改为五段 |
|
||||
| T2 | 第 22 号 Drekkana +7(`50847e57`) | 上游相关断言过;完整解读里该敏感点的字段名不变 |
|
||||
| T3 | 校正回放与重新冻结(红线 2) | 20/20、900/900 相同;新 `.freeze.json` 两份 + 回放 JSON 两份;`tests/test_sealed_holdout_contract_freshness.py` 全过 |
|
||||
| T4 | 上游配套文档 `docs/research/sensitive_point_drekkana_count_review_2026_10_14.md` 原路径放入;BUG_HISTORY 2 条、CHANGELOG、PROGRESS、`docs/tasks/README.md` 状态板 | — |
|
||||
|
||||
## 7. 让步顺序
|
||||
|
||||
T1、T3、T4 必做;T2 若与我方敏感点结构冲突,回报原因、不硬改。
|
||||
|
||||
## 8. 验收命令
|
||||
|
||||
| 检查 | Grok 自跑 | Claude 验收 |
|
||||
| --- | --- | --- |
|
||||
| 上游两个测试文件(放 `tests/test_upstream_sync9b_*.py`) | 必跑 | 复验 |
|
||||
| `tests/test_varga*.py`、`tests/test_cli_smoke.py -k "sarpa or drekkana or special"`、`tests/test_sealed_holdout_contract_freshness.py` | 必跑 | 复验 |
|
||||
| 校正 20 + 900 回放 | 必跑 | 复验 |
|
||||
| 前端 `report-fact-tables.test.ts` | 不跑(未改前端) | 跑 |
|
||||
| Python 快速门 | 不跑 | 以 Claude 为准 |
|
||||
|
||||
## 9. 开工前置命令
|
||||
|
||||
```
|
||||
git fetch origin
|
||||
git worktree add .worktrees/upstream-sync9b-20261028 codex/upstream-sync9b-20261028
|
||||
python scripts/pre_work_check.py --remote-timeout 8 --command-timeout 45
|
||||
grep -o 'BUG-[0-9]\+' docs/BUG_HISTORY.md | sort -t- -k2 -n | tail -1
|
||||
git fetch upstream main && git show 50847e57
|
||||
```
|
||||
|
||||
## 10. 真机
|
||||
|
||||
`docs/testing/upstream-sync9b-20261028.md`。
|
||||
@@ -0,0 +1,45 @@
|
||||
# TASK · 上游同步第九轮乙批 · 第 1 次打回(2026-10-28)
|
||||
|
||||
- 原任务书:`docs/tasks/TASK-upstream-sync9b-20261028.md`(issue #159)
|
||||
- 被验 SHA:`5c5f333d4d93fb691c10790988d3d4c986ac1bd3`(分支 `codex/upstream-sync9b-20261028`)
|
||||
- 执行方:Grok,同一分支继续提交;不新开 BUG 号
|
||||
|
||||
## 1. 验收结果
|
||||
|
||||
| 检查 | 结果 |
|
||||
| --- | --- |
|
||||
| 回报 SHA 是分支最新提交,可快进到当前 `origin/staging` | 是 |
|
||||
| `varga.py` 改动 | 与上游 `50847e57` 一致,只改了任务书点名的三处 |
|
||||
| 上游测试 + `test_varga_bphs.py` + 冻结契约 + 敏感点冒烟(Linux) | 49 过 |
|
||||
| 生时校正 20 条 + 900 条(Linux 复跑) | 与第八轮记录逐条相同 |
|
||||
| 旧冻结记录 | 逐字节未动 |
|
||||
| 两处偏离(临时记录回放、前端测试路径) | 认可,与第八轮做法一致 |
|
||||
| 任务书里 `-k "sarpa or drekkana or special"` 选中 0 条 | 这是任务书写错了,你改用真实测试名是对的 |
|
||||
|
||||
## 2. 失败项:冻结契约测试的排除清单被删了两条
|
||||
|
||||
`tests/test_sealed_holdout_contract_freshness.py::IDENTITY_FORBIDDEN_PREFIXES` 这份清单历来是**追加**的(每次重新冻结都「再排除」新路径,旧路径留着)。本分支把第八轮的两条:
|
||||
|
||||
```
|
||||
"docs/research/sealed_holdout_rerun_upstream_sync8_2026_10_18",
|
||||
"docs/research/reported_offset_upstream_sync8_2026_10_18",
|
||||
```
|
||||
|
||||
**替换**成了第九轮乙批的两条,第八轮那段注释还在、条目没了。这条测试守的是「冻结记录文件不能混进打分身份」,删掉以后第八轮的记录混进去也不会报错——测试仍然通过,但守门变弱了。
|
||||
|
||||
## 3. 要做的
|
||||
|
||||
1. 把上面两条第八轮路径加回去,放在原来那段注释下面;第九轮乙批两条保留在其后。即清单同时含第八轮与第九轮乙批共 4 条。
|
||||
2. PROGRESS「断言三栏」这一行改写为「新增两条、原有全部保留」。
|
||||
3. 跑 `tests/test_sealed_holdout_contract_freshness.py`,回报通过数。
|
||||
4. 前端:在工作树 `frontend/` 里 `npm ci` 后跑 `npm test -- tests/rectification-confirmation-gate.test.ts`(或该仓等价的单文件命令),回报结果。
|
||||
|
||||
## 4. 验收命令
|
||||
|
||||
| 检查 | Grok 自跑 | Claude 验收 |
|
||||
| --- | --- | --- |
|
||||
| `tests/test_sealed_holdout_contract_freshness.py` | 必跑 | 复验 |
|
||||
| `frontend/tests/rectification-confirmation-gate.test.ts` | 必跑 | 复验 |
|
||||
| 上游两个测试文件、校正 20 + 900 | 不必重跑(本次不改代码) | 复验 |
|
||||
|
||||
完成后照旧回报新的 head SHA。
|
||||
@@ -0,0 +1,9 @@
|
||||
# 真机走查 · 上游同步第九轮乙批(issue #159)
|
||||
|
||||
用虚构或公开出生资料,在 staging 上看:
|
||||
|
||||
| # | 操作 | 应该看到 |
|
||||
| --- | --- | --- |
|
||||
| 1 | 生成一份完整报告,翻到特殊点 / 敏感点表 | Sarpa Drekkana 行正常显示;没有空白或「undefined」 |
|
||||
| 2 | 同一张盘看第 22 号 Drekkana | 有星座,不报错 |
|
||||
| 3 | 发起一次生时校正,走到出结果 | 和以前一样能走完(校正打分没有变) |
|
||||
@@ -918,6 +918,8 @@ Admin 的 antd `<Spin>` 是独立设计系统,不在此表。
|
||||
|
||||
**「已对照 N 件」在手机上不在可见行里,桌面上目前仍在——这是一处未裁决的口径冲突。** 下面的禁列表写明这一项归交付卡、不上条,而它后来(cfb41daf)被加到了条上且没有改本文件。BUG-918 只把它在手机上移出可见行;桌面是否也撤掉需要产品裁决,见 `docs/BUG_HISTORY.md` BUG-918。
|
||||
|
||||
**代表分钟只写在条上(2026-10-28,issue #146)。** 盘面标题行不再写时刻,「看盘面」按钮只写这三个字(读屏名称仍带时刻和上升)。下面 D10 那段里「条上与盘面标题行同一次调用」从此只剩条这一处。
|
||||
|
||||
**代表分钟上条是 D10(2026-09-16)的产品决定,推翻了「条上只有区间」的原设计。** 理由是窄屏:`.is-compact` 下右栏盘面整体变成默认关闭的 overlay,代表分钟此前只在 `.rectification-board__clock` 出现,于是用户最该一直看着的那个数字恰恰看不见。条上的取值必须与盘面标题行**同一次调用**——`workingRectificationTime(result)`,由 `rectification-chat-view.ts` 的 `deriveRectificationChatView` 算一次(容器 `rectification-agentic-chat.tsx` 每次渲染调用它一次;2026-09-26 代码拆分只搬位置)、两处共用,不允许第二条推导路径。已采用时写「已采用 HH:MM」,否则写「代表分钟 HH:MM」;**采用不是确认**,完整边界句仍只在交付 / 采用旁白里出现一次,条上不写、也不得暗示某一分钟已确认。
|
||||
|
||||
**条上不得出现收窄进度(「已从 30 分钟收到 7 分钟」)。** 开场窗口确实在线上,但没有任何投影把这个对比作为字段给出,而 VOICE.md 第 2 条要求进度数字来自服务端。前端自己减还会算错:轴是**当前**窗口且会放宽(BUG-572),相减会把一次放宽报成进度。这句话继续由服务端拼进旁白。缺的字段见 `BLOCKED.md`。
|
||||
@@ -955,6 +957,14 @@ Admin 的 antd `<Spin>` 是独立设计系统,不在此表。
|
||||
|
||||
## 11. 区间交付卡
|
||||
|
||||
**卡头与按钮(2026-10-28,issue #147,产品同意 https://git.copse.top/root/Jyotisha/issues/147#issuecomment-36056 四条)。** 下文 D3、D4 与「更像这个」按以下改:
|
||||
|
||||
- 区分得开(D4:前两列差 ≥ 5 个百分点)且有代表分钟时,卡头是 `.rectification-result-lead`:小字「最可能的出生时间」、30px 衬线时刻、「上升X」(有该分钟的宫位表才写);「目前范围 …(对照了 N 件经历)」降为下一行 `.rectification-range-delivery__range-line`。区分不开时 D3 原样:范围是 `<strong>` 卡头,加「区分不开」那句,不写最可能。
|
||||
- 区分得开时最可能那列排第一,按钮是实心「采用 HH:MM 并打开星盘」,采用成功(服务端已保存)后去 `/chart`;其余列在小字「还不能排除」下面,按钮是描边「改用这个」,采用后不跳转。区分不开时每列仍是「更像这个」。出生记录冲突时一律「改用校正结果」,不跳转。
|
||||
- 不显示把握(低 / 中 / 高):引擎的这一档没有校准,BUG-560 同理。
|
||||
- 可以采用时,卡上多一个文字按钮「我说错了,改一条」,展开用户确认过的事件(与对话里的复述同一说法),点一条只在输入框里写好「我说错了:「…」应该是」,由用户补完发出,现有改记录工具来改;不自动发送、不另扣点。没有确认过的事件就不出这个按钮。不放「再补一件事」:训练门后补打字经历几乎缩不了范围,卡上不得邀请再补(BUG-1084,issue #148 产品 2026-10-28 定)。
|
||||
- 盘型交付卡(「用哪张盘解读」)不加时刻卡头(BUG-1151:盘型产品不排分钟),按钮文案不变;平常采用后同样去 `/chart`,也有上面那一行。
|
||||
|
||||
交付对象是区间里并排的候选分钟,不是支持度数字,也不是逐行空差异。
|
||||
|
||||
**相同的句子只写一次。** 两列或三列共有的 D9 / D10 / 月宿性格句提到卡片顶部(「三个时间的事业盘都在巨蟹座:做事以照顾人为主」)。列内只留有差别的句子;某列没有差别句就不显示性格块,不写「同上」。单列不提升共享句。
|
||||
@@ -973,6 +983,8 @@ Admin 的 antd `<Spin>` 是独立设计系统,不在此表。
|
||||
|
||||
## 12. 校正右栏盘面
|
||||
|
||||
**默认关着(2026-10-28,issue #146)。** 所有宽度上,头部都有「看盘面」按钮(`.rectification-board-peek`),盘面默认不出。桌面点开后在右侧分出一栏(`minmax(18rem, 22.5rem)`,没有候选时 16–18rem),280ms 从右淡入,标题行右侧是关闭按钮;手机仍是底部抽屉。标题行不写时刻。
|
||||
|
||||
右栏回答「当前按哪一分钟在看盘」,不回答「选哪一分钟」。顶部不加任何新说明文字。
|
||||
|
||||
**本命宫位改北印星盘。** 用 `VedicChartSvg` 渲染代表分钟的 `house_table`。宫位表退到星盘下方的 `<details>`(读屏与复制仍可用)。换分钟时只高亮变化的宫(`changedHouses` / `lagnaChanged`),不做动画。星盘组件在校正面 `dynamic()` 分包里,不进首页首屏。
|
||||
|
||||
@@ -63,6 +63,18 @@ Jyotisha 的可见文案是产品的一部分。正确性红线(真实性、
|
||||
|
||||
报告末尾「对哪一段想再问清楚?」列出前八节(含关系时间线,不含共同练习)。点进去是一条新对话,输入框预先写好这句,用户自己发送。中文预填是「合盘报告里「节名」这一段,在我们两人的盘里依据是什么?」英文报告用英文问同一件事。对话里可以说星曜、宫位、九分盘、大运,但只能引用这份报告已经写下的计算。材料里没有的,说「这份合盘没算到」。对不上的盘面,说「这一点在这份合盘里没有算到,我不能确定」。报告删了之后,输入框写「这份合盘已删除」,不能再发送。英文还没写好时只说「英文版即将提供」。选人页不写副标题。按钮是「开始合盘」,上面不写价格。点了先确认「会用掉 N 点,余额 M 点。」,按钮是「取消 / 开始」。余额不够时说「需要 N 点,余额只有 M 点。」,主按钮是「去补点数」。N 从计价表读,M 从账户读。计价读不到时不弹确认,只说「计费配置不可用」。这两人已经有写好的合盘、且没有一份正在生成时,按钮下面加「这两人已经合过 N 次,再生成会另扣 X 点」,N、X 都读实际数字。正在生成时不写这句。下拉占位是「选择一个人」。资料不齐写「资料未齐」,已经选在另一边写「已在另一边」。档案满了写「档案已满(最多 20 人)」,旁边是「去星盘档案删一个」。新增星盘卡片用「称呼」「性别」「出生日期」「出生时间」「出生地」,按钮是「取消」和「保存并选中」。产品关着时说「合盘服务暂未开放」。开关读不到时说「合盘服务暂时连不上,请稍后再试」。开关结果回来之前,选人页不出现两个人的选择框,也不出现「开始合盘」。英文次数用尽时说「英文版暂时无法再试」。生成中说「正在写 甲 × 乙 的合盘报告…」,四步是「算两张完整命盘」「整理两人之间的证据」「写报告」「检查报告」,下面是「大约要几分钟。可以先离开,写好后在「之前的合盘」和「报告」里都能找到。」英文等待说「正在写英文版…」和「第一次切到英文时才写,不扣点,大约一两分钟。」报告标题下是「已保存,再次打开不扣点」。
|
||||
|
||||
## 生时校正对话:开场说来源、事件没起作用时直说(2026-10-28,issue #148)
|
||||
|
||||
- **开场窗口句带上时间来源**,仍是一句:「我们来把你的出生时间缩小到更准的范围,按你填的大概时间,现在先在 HH:MM–HH:MM 之间找。」盘型产品:「……能不能选定用来解读的星盘,按你的出生记录时间,核对窗口是 HH:MM–HH:MM。」来源词只有四种:按你的出生记录时间 / 按你填的大概时间 / 按你填的时间(家人记得的准确时间)/ 按你给的时间段;不知道或旧资料不写。来源已知时开场由服务器写,模型那一版不用。不追问误差(不确定度只在资料里问一次)。
|
||||
- **事件轮范围没动**:这一轮记下了事件、服务器重算过、又不是出结果的那一轮,范围前后完全一样时,正文后面接一句「这件事在现在的范围里每个时刻都对得上,分不出前后。」范围变了照旧写「范围从 … 变为 …」;没重新比较照旧写「这次没有重新比较……」。这句不邀请再补经历(BUG-1084)。
|
||||
- 不做:逐层播报上升、月宿、大运变不变(BUG-606、BUG-1253);按精度限制能用哪些层(属于评分);追问离职是主动还是被动(引擎用不上);每轮写还缺什么、补哪类事能再收窄(BUG-1084)。
|
||||
|
||||
## 生时校正界面:收束卡与盘面(2026-10-28,issue #146、#147)
|
||||
|
||||
- 头部按钮「看盘面」;盘面标题「当前盘面」,不写时刻。
|
||||
- 收束卡区分得开时:「最可能的出生时间」+ 时刻 + 「上升X」,下一行仍是「目前范围 …(对照了 N 件经历)」。最可能那列按钮「采用 HH:MM 并打开星盘」;其余列上方小字「还不能排除」,按钮「改用这个」。区分不开时不变(「更像这个」、「这几个时刻按现在的方法区分不开。」)。不写「把握」。
|
||||
- 卡上一个文字按钮「我说错了,改一条」(不放「再补一件事」,BUG-1084);展开后一句「选一条要改的,输入框里会先写好开头:」,点一条后输入框写「我说错了:「事件」应该是」。
|
||||
|
||||
## 生时校正独立入口(2026-10-28,issue #145)
|
||||
|
||||
产品 2026-10-28 定:校正有自己的入口,不再从对话里开案例。下面这节「生时校正从对话进入」里的卡片文案和「明确要求则直接进入」由本节取代,其余(何时提议、做不了必须说一句、正文怎么提、回来)不变。
|
||||
|
||||
@@ -2827,14 +2827,26 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(18rem, 22.5rem);
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
background: var(--color-canvas);
|
||||
}
|
||||
/* Issue #146: the board is closed by default; 「看盘面」 in the header opens it
|
||||
as a right column on wide screens. */
|
||||
.rectification-workspace.is-board-open:not(.is-compact) {
|
||||
grid-template-columns: minmax(0, 1fr) minmax(18rem, 22.5rem);
|
||||
}
|
||||
/* No candidate yet: the board only carries the declared time, so it takes less
|
||||
width until there is a house table to show. */
|
||||
.rectification-workspace.is-board-empty {
|
||||
.rectification-workspace.is-board-empty.is-board-open:not(.is-compact) {
|
||||
grid-template-columns: minmax(0, 1fr) minmax(16rem, 18rem);
|
||||
}
|
||||
.rectification-workspace:not(.is-compact) .rectification-board {
|
||||
animation: rectification-board-enter 280ms cubic-bezier(.22, 1, .36, 1) both;
|
||||
}
|
||||
@keyframes rectification-board-enter {
|
||||
from { opacity: 0; transform: translateX(16px); }
|
||||
to { opacity: 1; transform: none; }
|
||||
}
|
||||
.rectification-workspace.is-compact {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
grid-template-rows: minmax(0, 1fr);
|
||||
@@ -7201,3 +7213,83 @@ button.synastry-dd-add { border-radius: 0 0 var(--radius-md) var(--radius-md); }
|
||||
.starter-rectify-link:hover {
|
||||
background: var(--color-canvas-muted);
|
||||
}
|
||||
|
||||
/* Result card (issue #147). The most likely minute leads only when the card
|
||||
says the columns can be told apart (D4); the range drops to the line under
|
||||
it. Candidates that cannot be ruled out keep their own adopt button. */
|
||||
.rectification-result-lead {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
gap: var(--space-1) var(--space-3);
|
||||
}
|
||||
.rectification-result-lead__label {
|
||||
color: var(--color-ink-tertiary);
|
||||
font-size: var(--type-body-sm);
|
||||
}
|
||||
.rectification-result-lead__time {
|
||||
color: var(--color-ink);
|
||||
font-family: var(--font-display);
|
||||
font-size: 30px;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.rectification-result-lead__lagna {
|
||||
color: var(--color-ink-secondary);
|
||||
font-size: var(--type-body-sm);
|
||||
}
|
||||
.rectification-range-delivery__range-line {
|
||||
margin: 0;
|
||||
color: var(--color-ink-secondary);
|
||||
font-size: var(--type-body-sm);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.rectification-range-delivery__others {
|
||||
margin: 0 0 var(--space-2);
|
||||
padding-top: var(--space-3);
|
||||
border-top: 1px solid var(--color-canvas-muted);
|
||||
color: var(--color-ink-tertiary);
|
||||
font-size: var(--type-caption);
|
||||
}
|
||||
.rectification-result-actions {
|
||||
display: grid;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
.rectification-result-actions__row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
.rectification-result-actions__fix {
|
||||
color: var(--color-action);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
.rectification-result-actions__list {
|
||||
display: grid;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-3);
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--color-canvas-soft);
|
||||
animation: rectification-result-list-enter 260ms cubic-bezier(.22, 1, .36, 1) both;
|
||||
}
|
||||
.rectification-result-actions__list p {
|
||||
margin: 0;
|
||||
color: var(--color-ink-secondary);
|
||||
font-size: var(--type-body-sm);
|
||||
}
|
||||
.rectification-result-actions__items {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
.rectification-result-actions__items button {
|
||||
white-space: normal;
|
||||
text-align: start;
|
||||
}
|
||||
@keyframes rectification-result-list-enter {
|
||||
from { opacity: 0; transform: translateY(8px); }
|
||||
to { opacity: 1; transform: none; }
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import { createPortal } from "react-dom";
|
||||
import type { ChatMessage } from "@/lib/chat-message-view";
|
||||
import { canShowRectificationSelectionCards } from "@/lib/rectification-candidate-result";
|
||||
import { RectificationRangeDelivery } from "@/components/rectification-range-delivery";
|
||||
import { useOpenChartAfterAdopt } from "@/hooks/use-open-chart-after-adopt";
|
||||
import { fixEvidenceDraft } from "@/lib/rectification-fix-evidence";
|
||||
import { livingDisplayFromReceipt } from "@/lib/rectification-agentic/core/candidate-window";
|
||||
import {
|
||||
RECTIFICATION_EMPTY_ACTION_LABEL,
|
||||
@@ -170,7 +172,9 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
currentQuestionRef,
|
||||
applyCaseSnapshot,
|
||||
loadCaseSnapshot,
|
||||
fixableEvidence,
|
||||
} = useRectificationCaseSnapshot({ caseId, sessionId, initialSnapshot });
|
||||
const { arm: armChartAfterAdopt, onSaved: savedThenChart } = useOpenChartAfterAdopt(onSaved);
|
||||
const readonly = historyReadonly || Boolean(resultNotice);
|
||||
const conversationReadonly = historyReadonly || (Boolean(resultNotice) && !canRecompare);
|
||||
const [messages, setMessages] = useState<RenderMessage[]>(() => messagesFromTurns(initialTurns));
|
||||
@@ -365,30 +369,33 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
void send("opening", "");
|
||||
}, [initialTurns.length, onOpeningConsumed, readonly, send, shouldStartOpening]);
|
||||
|
||||
const acceptCandidate = useCallback(async (candidateId: string) => runRectificationCandidateAccept({
|
||||
caseId,
|
||||
sessionId,
|
||||
busy,
|
||||
readonly,
|
||||
candidateResult,
|
||||
acceptingCandidateId,
|
||||
keyCounter,
|
||||
runAbort,
|
||||
lastRunOutcome,
|
||||
conversationAnchor,
|
||||
setError,
|
||||
setMessages,
|
||||
setAcceptingCandidateId,
|
||||
setCandidateResult,
|
||||
setSavedTime,
|
||||
setSavedStatus,
|
||||
setPending,
|
||||
beginLiveRun,
|
||||
loadCaseSnapshot,
|
||||
send,
|
||||
onCompleted,
|
||||
onSaved,
|
||||
}, candidateId), [acceptingCandidateId, beginLiveRun, busy, candidateResult, caseId, loadCaseSnapshot, onCompleted, onSaved, readonly, send, sessionId, setPending]);
|
||||
const acceptCandidate = useCallback(async (candidateId: string, openChart = false) => {
|
||||
armChartAfterAdopt(openChart);
|
||||
return runRectificationCandidateAccept({
|
||||
caseId,
|
||||
sessionId,
|
||||
busy,
|
||||
readonly,
|
||||
candidateResult,
|
||||
acceptingCandidateId,
|
||||
keyCounter,
|
||||
runAbort,
|
||||
lastRunOutcome,
|
||||
conversationAnchor,
|
||||
setError,
|
||||
setMessages,
|
||||
setAcceptingCandidateId,
|
||||
setCandidateResult,
|
||||
setSavedTime,
|
||||
setSavedStatus,
|
||||
setPending,
|
||||
beginLiveRun,
|
||||
loadCaseSnapshot,
|
||||
send,
|
||||
onCompleted,
|
||||
onSaved: savedThenChart,
|
||||
}, candidateId);
|
||||
}, [acceptingCandidateId, armChartAfterAdopt, savedThenChart, beginLiveRun, busy, candidateResult, caseId, loadCaseSnapshot, onCompleted, readonly, send, sessionId, setPending]);
|
||||
|
||||
async function copyMessage(message: RenderMessage) {
|
||||
await copyRectificationMessage({ setCopiedMessageKey }, message);
|
||||
@@ -533,7 +540,9 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
};
|
||||
});
|
||||
|
||||
const boardPeek = compactBoard && !boardOpen ? (
|
||||
// Issue #146: 「看盘面」 is in the header at every width. On a phone the open
|
||||
// sheet covers it; on a desktop it stays as the toggle beside the open board.
|
||||
const boardPeek = !compactBoard || !boardOpen ? (
|
||||
<RectificationBoardPeek
|
||||
result={candidateResult}
|
||||
declaredTime={declaredTime}
|
||||
@@ -598,8 +607,13 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
result={candidateResult}
|
||||
acceptingCandidateId={acceptingCandidateId}
|
||||
readonly={readonly || (busy && !acceptingCandidateId)}
|
||||
onAccept={(candidateId) => void acceptCandidate(candidateId)}
|
||||
onAccept={(candidateId, openChart) => void acceptCandidate(candidateId, openChart)}
|
||||
adoptLocked={liveInterviewBlockingAdopt}
|
||||
fixableEvidence={fixableEvidence}
|
||||
onFixEvidence={(line) => {
|
||||
setDraft(fixEvidenceDraft(line));
|
||||
composer.current?.focus({ preventScroll: true });
|
||||
}}
|
||||
/>
|
||||
{verifiedIdleCopy && (
|
||||
<p className="rectification-pending-note" role="status">
|
||||
|
||||
@@ -94,12 +94,9 @@ function RectificationBoardBody({
|
||||
<header className="rectification-board__header">
|
||||
<div className="rectification-board__title-row">
|
||||
<h2 id={titleId}>当前盘面</h2>
|
||||
{workingTime
|
||||
? <span className="rectification-board__clock">{workingTime}</span>
|
||||
: emptyCopy.clock
|
||||
? <span className="rectification-board__clock is-declared">{emptyCopy.clock}</span>
|
||||
: null}
|
||||
{compact && (
|
||||
{/* Issue #146: the working minute is written once, on the timeline
|
||||
above the conversation. The board no longer repeats it. */}
|
||||
{(
|
||||
<Button
|
||||
className="rectification-board__close"
|
||||
type="button"
|
||||
@@ -288,7 +285,8 @@ export function RectificationBoard({
|
||||
return () => window.removeEventListener("keydown", handleKey);
|
||||
}, [compact, open, onClose]);
|
||||
|
||||
if (compact && !open) return null;
|
||||
// Issue #146: closed by default at every width; 「看盘面」 opens it.
|
||||
if (!open) return null;
|
||||
|
||||
const sheet = (
|
||||
<aside
|
||||
@@ -323,6 +321,9 @@ export function RectificationBoard({
|
||||
);
|
||||
}
|
||||
|
||||
/** Issue #146: the header button says what it does; the time lives on the timeline. */
|
||||
export const RECTIFICATION_BOARD_OPEN_LABEL = "看盘面";
|
||||
|
||||
export function RectificationBoardPeek({
|
||||
result,
|
||||
declaredTime = null,
|
||||
@@ -344,13 +345,12 @@ export function RectificationBoardPeek({
|
||||
className="rectification-board-peek"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
aria-label={peek.detail ? `${peek.title},${peek.detail}` : peek.title}
|
||||
aria-label={peek.detail ? `${RECTIFICATION_BOARD_OPEN_LABEL},${peek.detail}` : RECTIFICATION_BOARD_OPEN_LABEL}
|
||||
aria-expanded={expanded}
|
||||
aria-controls={boardId}
|
||||
onClick={onOpen}
|
||||
>
|
||||
<strong>{peek.title}</strong>
|
||||
<span>{peek.detail}</span>
|
||||
<strong>{RECTIFICATION_BOARD_OPEN_LABEL}</strong>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,12 @@ import { RectificationVerificationReport } from "@/components/rectification-veri
|
||||
import { RectificationSegmentDelivery } from "@/components/rectification-segment-delivery";
|
||||
import { sessionOutcomeAllowsAdopt } from "@/lib/rectification-agentic/core/rectification-decision";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { RectificationResultActions, RectificationResultLead } from "@/components/rectification-result-parts";
|
||||
import {
|
||||
RECTIFICATION_OTHER_CANDIDATES_LABEL,
|
||||
RECTIFICATION_SWITCH_TO_THIS_LABEL,
|
||||
adoptAndOpenChartLabel,
|
||||
} from "@/lib/rectification-fix-evidence";
|
||||
|
||||
import { deliveryWindowFromReceipt, protectVargaReport } from "@/lib/rectification-agentic/v9/delivery-window";
|
||||
|
||||
@@ -31,12 +37,17 @@ export function RectificationRangeDelivery({
|
||||
readonly,
|
||||
onAccept,
|
||||
adoptLocked,
|
||||
fixableEvidence = [],
|
||||
onFixEvidence,
|
||||
}: Readonly<{
|
||||
result: RectificationCandidateResult;
|
||||
acceptingCandidateId: string | null;
|
||||
readonly: boolean;
|
||||
onAccept: (candidateId: string) => void;
|
||||
/** `openChart`: the most likely column's 「采用 HH:MM 并打开星盘」 (issue #147). */
|
||||
onAccept: (candidateId: string, openChart?: boolean) => void;
|
||||
adoptLocked?: boolean;
|
||||
fixableEvidence?: readonly string[];
|
||||
onFixEvidence?: (line: string) => void;
|
||||
}>) {
|
||||
const delivery: RangeDeliveryProjection | null = result.rangeDelivery;
|
||||
const range = delivery?.range ?? result.credibleRange;
|
||||
@@ -71,24 +82,48 @@ export function RectificationRangeDelivery({
|
||||
// D4: per-column numbers only when the first column leads by ≥ 5 points.
|
||||
const showPercents = rangeDeliveryShowsPercents(columns);
|
||||
const indistinct = columns.length >= 2 && !showPercents;
|
||||
// Issue #147: lead with the most likely minute only when D4 says the columns
|
||||
// can be told apart; otherwise the range stays the headline (D3).
|
||||
const primaryColumn = mostLikely && !indistinct
|
||||
? columns.find((column) => column.candidate_id === delivery?.representative_candidate_id)
|
||||
?? columns.find((column) => column.time === mostLikely)
|
||||
?? null
|
||||
: null;
|
||||
const primaryLagna = primaryColumn
|
||||
? result.houseTablesByTime[primaryColumn.time]?.lagna
|
||||
?? (result.houseTable?.time === primaryColumn.time ? result.houseTable.lagna : null)
|
||||
?? null
|
||||
: null;
|
||||
const orderedColumns = primaryColumn
|
||||
? [primaryColumn, ...columns.filter((column) => column !== primaryColumn)]
|
||||
: columns;
|
||||
const showActions = adoptAllowed && !adoptLocked && !readonly;
|
||||
|
||||
if (delivery && (delivery.product_delivery || delivery.segment_summary)) {
|
||||
return <RectificationSegmentDelivery summary={delivery.segment_summary ?? null} delivery={delivery} rangeLabel={rangeLabel}
|
||||
selection={result.segmentSelection} resultId={result.resultId} busy={Boolean(busy)} accepting={Boolean(acceptingCandidateId)} onAccept={onAccept}
|
||||
adoptLocked={adoptLocked} markdown={markdown} canAdopt={adoptAllowed}
|
||||
candidateDate={result.candidates.find((candidate) => candidate.candidateId === delivery.representative_candidate_id)?.candidate_date} />;
|
||||
candidateDate={result.candidates.find((candidate) => candidate.candidateId === delivery.representative_candidate_id)?.candidate_date}
|
||||
fixableEvidence={fixableEvidence} onFixEvidence={onFixEvidence} readonly={readonly} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="rectification-candidates rectification-range-delivery" aria-label="生时校正区间">
|
||||
<div className="rectification-candidates-heading">
|
||||
<strong>{title}</strong>
|
||||
{mostLikely && !indistinct ? (
|
||||
<span className="rectification-range-delivery__most-likely">
|
||||
{rangeDeliveryMostLikely(mostLikely)}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
{primaryColumn ? (
|
||||
<>
|
||||
<RectificationResultLead time={primaryColumn.time} lagna={primaryLagna} />
|
||||
<p className="rectification-range-delivery__range-line">{title}</p>
|
||||
</>
|
||||
) : (
|
||||
<div className="rectification-candidates-heading">
|
||||
<strong>{title}</strong>
|
||||
{mostLikely && !indistinct ? (
|
||||
<span className="rectification-range-delivery__most-likely">
|
||||
{rangeDeliveryMostLikely(mostLikely)}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
{vargaBlocked ? (
|
||||
<p className="rectification-range-delivery__narrow">{windowMinutes === null ? "完整窗口范围暂不可用;暂不推荐分盘解读。" : "分盘解读需要更准的出生时间"}</p>
|
||||
) : caption ? (
|
||||
@@ -118,7 +153,8 @@ export function RectificationRangeDelivery({
|
||||
</ul>
|
||||
) : null}
|
||||
<ul className="rectification-range-delivery__columns">
|
||||
{columns.map((column) => {
|
||||
{orderedColumns.map((column, index) => {
|
||||
const isPrimary = column === primaryColumn;
|
||||
const candidateDate = result.candidates.find((candidate) => candidate.candidateId === column.candidate_id)?.candidate_date;
|
||||
const relativeDay = candidateDate && result.declaredBirthDate && candidateDate !== result.declaredBirthDate
|
||||
? candidateDate < result.declaredBirthDate ? "(前一天)" : "(后一天)" : "";
|
||||
@@ -128,8 +164,11 @@ export function RectificationRangeDelivery({
|
||||
return (
|
||||
<li
|
||||
key={column.candidate_id}
|
||||
className={`rectification-range-delivery__column${adopted ? " is-adopted" : ""}`}
|
||||
className={`rectification-range-delivery__column${adopted ? " is-adopted" : ""}${isPrimary ? " is-primary" : ""}`}
|
||||
>
|
||||
{primaryColumn && index === 1 ? (
|
||||
<p className="rectification-range-delivery__others">{RECTIFICATION_OTHER_CANDIDATES_LABEL}</p>
|
||||
) : null}
|
||||
<div className="rectification-range-delivery__time">{candidateDate ? `${candidateDate}${relativeDay} ${column.time}` : column.time}</div>
|
||||
{adoptAllowed && !adoptLocked && candidateDate ? <p className="rectification-range-delivery__muted">采用后,排盘使用这一日期和时间;原填报日期保留。</p> : null}
|
||||
{showPercents ? (
|
||||
@@ -162,11 +201,11 @@ export function RectificationRangeDelivery({
|
||||
<>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
variant={isPrimary ? "default" : "outline"}
|
||||
className="rectification-range-delivery__accept"
|
||||
aria-pressed={adopted}
|
||||
disabled={busy || adopted}
|
||||
onClick={() => onAccept(column.candidate_id)}
|
||||
onClick={() => onAccept(column.candidate_id, isPrimary && !recordConflict)}
|
||||
>
|
||||
{adopting
|
||||
? RECTIFICATION_USER_COPY.rangeDeliveryAdopting
|
||||
@@ -174,7 +213,11 @@ export function RectificationRangeDelivery({
|
||||
? RECTIFICATION_USER_COPY.rangeDeliveryAdopted
|
||||
: recordConflict
|
||||
? SWITCH_TO_RECTIFIED_LABEL
|
||||
: RECTIFICATION_USER_COPY.rangeDeliveryMoreLikeThis}
|
||||
: isPrimary
|
||||
? adoptAndOpenChartLabel(column.time)
|
||||
: primaryColumn
|
||||
? RECTIFICATION_SWITCH_TO_THIS_LABEL
|
||||
: RECTIFICATION_USER_COPY.rangeDeliveryMoreLikeThis}
|
||||
</Button>
|
||||
{recordConflict && !adopted ? (
|
||||
<p className="rectification-range-delivery__switch-note">
|
||||
@@ -189,6 +232,13 @@ export function RectificationRangeDelivery({
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
{showActions ? (
|
||||
<RectificationResultActions
|
||||
evidence={fixableEvidence}
|
||||
disabled={Boolean(busy)}
|
||||
onFixEvidence={onFixEvidence}
|
||||
/>
|
||||
) : null}
|
||||
{delivery?.more_label ? (
|
||||
<p className="rectification-range-delivery__more">{delivery.more_label}</p>
|
||||
) : null}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
"use client";
|
||||
|
||||
import { useId, useState } from "react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { RECTIFICATION_FIX_COPY, RECTIFICATION_MOST_LIKELY_LABEL } from "@/lib/rectification-fix-evidence";
|
||||
|
||||
/**
|
||||
* Issue #147: the result card leads with the most likely minute when the
|
||||
* server says the candidates can be told apart (D4). The card decides that;
|
||||
* this only draws it.
|
||||
*/
|
||||
export function RectificationResultLead({ time, lagna }: Readonly<{ time: string; lagna: string | null }>) {
|
||||
return (
|
||||
<div className="rectification-result-lead">
|
||||
<span className="rectification-result-lead__label">{RECTIFICATION_MOST_LIKELY_LABEL}</span>
|
||||
<strong className="rectification-result-lead__time">{time}</strong>
|
||||
{lagna ? <span className="rectification-result-lead__lagna">上升{lagna}</span> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 「我说错了,改一条」 lists the confirmed events and fills the composer with
|
||||
* the chosen one. Nothing is sent here. There is deliberately no 「再补一件事」:
|
||||
* typed events barely narrow the range after the training gate, so the card
|
||||
* must not invite more (BUG-1084, issue #148 decision 2026-10-28).
|
||||
*/
|
||||
export function RectificationResultActions({
|
||||
evidence,
|
||||
disabled,
|
||||
onFixEvidence,
|
||||
}: Readonly<{
|
||||
evidence: readonly string[];
|
||||
disabled: boolean;
|
||||
onFixEvidence?: (line: string) => void;
|
||||
}>) {
|
||||
const [fixOpen, setFixOpen] = useState(false);
|
||||
const listId = useId();
|
||||
if (!onFixEvidence || evidence.length === 0) return null;
|
||||
return (
|
||||
<div className="rectification-result-actions">
|
||||
<div className="rectification-result-actions__row">
|
||||
<Button
|
||||
type="button"
|
||||
variant="link"
|
||||
className="rectification-result-actions__fix"
|
||||
disabled={disabled}
|
||||
aria-expanded={fixOpen}
|
||||
aria-controls={listId}
|
||||
onClick={() => setFixOpen((open) => !open)}
|
||||
>
|
||||
{RECTIFICATION_FIX_COPY.fixOne}
|
||||
</Button>
|
||||
</div>
|
||||
{fixOpen ? (
|
||||
<div id={listId} className="rectification-result-actions__list" role="group" aria-label={RECTIFICATION_FIX_COPY.fixOne}>
|
||||
<p>{RECTIFICATION_FIX_COPY.pickHint}</p>
|
||||
<div className="rectification-result-actions__items">
|
||||
{evidence.map((line) => (
|
||||
<Button
|
||||
key={line}
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
setFixOpen(false);
|
||||
onFixEvidence(line);
|
||||
}}
|
||||
>
|
||||
{line}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -8,8 +8,10 @@ import { RectificationVerificationReport } from "@/components/rectification-veri
|
||||
import { protectVargaReport } from "@/lib/rectification-agentic/v9/delivery-window";
|
||||
import { unknownProductDeliveryCopy } from "@/lib/rectification-agentic/v9/product-delivery";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { RectificationResultActions } from "@/components/rectification-result-parts";
|
||||
|
||||
export function RectificationSegmentDelivery({ summary, delivery, rangeLabel, selection, resultId, busy, accepting, onAccept, adoptLocked, markdown, candidateDate, canAdopt = false }: Readonly<{
|
||||
export function RectificationSegmentDelivery({ summary, delivery, rangeLabel, selection, resultId, busy, accepting, onAccept, adoptLocked, markdown, candidateDate, canAdopt = false,
|
||||
fixableEvidence = [], onFixEvidence, readonly = false }: Readonly<{
|
||||
summary: SegmentSummary | null;
|
||||
delivery: RangeDeliveryProjection;
|
||||
rangeLabel: string | null;
|
||||
@@ -17,11 +19,15 @@ export function RectificationSegmentDelivery({ summary, delivery, rangeLabel, se
|
||||
resultId: string;
|
||||
busy: boolean;
|
||||
accepting: boolean;
|
||||
onAccept: (candidateId: string) => void;
|
||||
/** `openChart`: 「采用 HH:MM 并打开星盘」 (issue #147). */
|
||||
onAccept: (candidateId: string, openChart?: boolean) => void;
|
||||
adoptLocked?: boolean;
|
||||
markdown?: string | null;
|
||||
candidateDate?: string;
|
||||
canAdopt?: boolean;
|
||||
fixableEvidence?: readonly string[];
|
||||
onFixEvidence?: (line: string) => void;
|
||||
readonly?: boolean;
|
||||
}>) {
|
||||
const adoption = summary?.adoption_minute;
|
||||
const candidateId = delivery.representative_candidate_id;
|
||||
@@ -36,6 +42,10 @@ export function RectificationSegmentDelivery({ summary, delivery, rangeLabel, se
|
||||
const blocked = summary?.charts.some((chart) => chart.tier === "blocked") === true;
|
||||
const reportBlocked = !summary || blocked;
|
||||
const showAdopt = Boolean(canAdopt && adoption && candidateId && !adoptLocked);
|
||||
// Issue #147: no minute lead here. The segment product answers "which chart",
|
||||
// never "which minute" (BUG-1151, product 10-01 option A). A plain adopt still
|
||||
// opens the chart afterwards; re-adopt and record-conflict adopts do not.
|
||||
const plainAdopt = !(readopt && priorAdoption) && !delivery.record_conflict;
|
||||
return (
|
||||
<section className="rectification-candidates rectification-range-delivery" aria-label="生时校正盘型">
|
||||
<div className="rectification-candidates-heading"><strong>用哪张盘解读</strong></div>
|
||||
@@ -58,8 +68,8 @@ export function RectificationSegmentDelivery({ summary, delivery, rangeLabel, se
|
||||
{showAdopt && adoption && candidateId ? (
|
||||
<>
|
||||
<p className="rectification-range-delivery__muted">采用后按 {adoption.date ? `${adoption.date} ` : ""}{adoption.time} 排盘;原填报资料保留。</p>
|
||||
<Button type="button" variant="outline" className="rectification-range-delivery__accept" disabled={busy || adopted}
|
||||
aria-pressed={adopted} onClick={() => onAccept(candidateId)}>
|
||||
<Button type="button" variant={plainAdopt ? "default" : "outline"} className="rectification-range-delivery__accept" disabled={busy || adopted}
|
||||
aria-pressed={adopted} onClick={() => onAccept(candidateId, plainAdopt)}>
|
||||
{accepting ? RECTIFICATION_USER_COPY.rangeDeliveryAdopting : adopted ? RECTIFICATION_USER_COPY.rangeDeliveryAdopted
|
||||
: readopt && priorAdoption ? `改用 ${readopt}`
|
||||
: delivery.record_conflict ? SWITCH_TO_RECTIFIED_LABEL : `改用 ${charts.map((chart) => chart.chart).join(" + ")} 盘解读`}
|
||||
@@ -76,6 +86,9 @@ export function RectificationSegmentDelivery({ summary, delivery, rangeLabel, se
|
||||
) : adopted ? (
|
||||
<p className="rectification-range-delivery__muted">{RECTIFICATION_USER_COPY.rangeDeliveryAdopted}</p>
|
||||
) : null}
|
||||
{showAdopt && !readonly ? (
|
||||
<RectificationResultActions evidence={fixableEvidence} disabled={busy} onFixEvidence={onFixEvidence} />
|
||||
) : null}
|
||||
<p className="rectification-range-delivery__boundary">{delivery.boundary || REPRESENTATIVE_MINUTE_DISCLAIMER}</p>
|
||||
<RectificationVerificationReport markdown={protectVargaReport(markdown === undefined ? delivery.verification_markdown : markdown, reportBlocked, delivery.product_delivery?.targets ?? summary?.targets,
|
||||
!summary ? "完整窗口扫描暂不可用;不能提供分盘解读推荐。" : undefined)} />
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useRef } from "react";
|
||||
|
||||
import { navigateInApp } from "@/lib/app-navigation";
|
||||
|
||||
/**
|
||||
* 「采用 HH:MM 并打开星盘」 (issue #147). The adopt itself is the existing
|
||||
* accept run; this only goes to /chart once the server has saved the time.
|
||||
* Armed per click, so a failed adopt or a plain 「改用这个」 never navigates.
|
||||
*/
|
||||
export function useOpenChartAfterAdopt(
|
||||
onSaved: ((time: string, status: "accepted" | "confirmed") => void) | undefined,
|
||||
) {
|
||||
const openAfterSave = useRef(false);
|
||||
const arm = useCallback((open: boolean) => {
|
||||
openAfterSave.current = open;
|
||||
}, []);
|
||||
const saved = useCallback((time: string, status: "accepted" | "confirmed") => {
|
||||
onSaved?.(time, status);
|
||||
if (!openAfterSave.current) return;
|
||||
openAfterSave.current = false;
|
||||
navigateInApp("/chart");
|
||||
}, [onSaved]);
|
||||
return { arm, onSaved: saved };
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
type CurrentQuestionModel,
|
||||
} from "@/lib/rectification-chat-snapshot";
|
||||
import type { CandidateResult } from "@/lib/rectification-chat-snapshot";
|
||||
import { fixableEvidenceLines } from "@/lib/rectification-fix-evidence";
|
||||
|
||||
/**
|
||||
* Case snapshot sync for the rectification chat surface: the state that the
|
||||
@@ -63,6 +64,8 @@ export function useRectificationCaseSnapshot({
|
||||
const [questionRepairAttempts, setQuestionRepairAttempts] = useState(0);
|
||||
const [questionRepairing, setQuestionRepairing] = useState(false);
|
||||
const [snapshotFailure, setSnapshotFailure] = useState<RectificationSnapshotFailure | null>(null);
|
||||
// Issue #147: the confirmed events 「我说错了,改一条」 lists.
|
||||
const [fixableEvidence, setFixableEvidence] = useState<readonly string[]>(() => fixableEvidenceLines(initialSnapshot?.evidence));
|
||||
const snapshotAbort = useRef<AbortController | null>(null);
|
||||
const currentQuestionRef = useRef(currentQuestion);
|
||||
|
||||
@@ -93,6 +96,7 @@ export function useRectificationCaseSnapshot({
|
||||
const snapshot = payload as RectificationCaseSnapshotPayload;
|
||||
setResultNotice(snapshot.case?.result_notice ?? null);
|
||||
setCanRecompare(snapshot.case?.result_identity?.can_recompare === true);
|
||||
if (Array.isArray(snapshot.evidence)) setFixableEvidence(fixableEvidenceLines(snapshot.evidence));
|
||||
const nextCandidate = parseRectificationCandidateResult(payload.latest_result);
|
||||
const nextQuestion = currentQuestionFromSnapshot(payload.current_question);
|
||||
const nextChoice = parseRectificationChoiceCard(payload.choice_card);
|
||||
@@ -174,6 +178,7 @@ export function useRectificationCaseSnapshot({
|
||||
}, [applyCaseSnapshot, caseId, sessionId]);
|
||||
|
||||
return {
|
||||
fixableEvidence,
|
||||
resultNotice,
|
||||
canRecompare,
|
||||
savedTime,
|
||||
|
||||
@@ -119,12 +119,16 @@ export function openingSpokenBody(
|
||||
range: readonly [string, string] | null,
|
||||
productDelivery?: ProductDelivery | null,
|
||||
facts?: OpeningBoundaryFacts | null,
|
||||
source?: string | null,
|
||||
): string {
|
||||
const window = formatClockRange(range);
|
||||
// Issue #148: the window sentence also says where the time came from. Still
|
||||
// one sentence, so the opening stays three plain layers.
|
||||
const from = window ? openingSourcePhrase(source) : null;
|
||||
const head = productDelivery
|
||||
? `我们先判断现在的出生时间能不能选定用来解读的星盘${window ? `,核对窗口是 ${window}` : ""}。`
|
||||
? `我们先判断现在的出生时间能不能选定用来解读的星盘${window ? `,${from ? `${from},` : ""}核对窗口是 ${window}` : ""}。`
|
||||
: window
|
||||
? `我们来把你的出生时间缩小到更准的范围,现在先在 ${window} 之间找。`
|
||||
? `我们来把你的出生时间缩小到更准的范围,${from ? `${from},` : ""}现在先在 ${window} 之间找。`
|
||||
: "我们来把你的出生时间缩小到更准的范围。";
|
||||
const how = "做法很简单:你说几件人生里的大事和大概年月,我拿去和星盘对照。";
|
||||
const extra = openingBoundarySentence(facts);
|
||||
@@ -269,6 +273,8 @@ export const RECTIFICATION_USER_COPY = {
|
||||
forceMinuteAfterSubBlocks: "时段分不开,直接按分钟比。",
|
||||
compareFailedRetry: "这次比较没跑成,你说下一句的时候我会再试一次。",
|
||||
rescoreSkipped: "这次没有重新比较,等下会再比一次。",
|
||||
/** Issue #148: a recorded event that left the range exactly where it was. */
|
||||
evidenceNoSplit: "这件事在现在的范围里每个时刻都对得上,分不出前后。",
|
||||
/** BUG-1178: an event dated after today is not recorded or scored. */
|
||||
futureDatedEvent: "有一件事的日期在今天之后,这件先不算;如果年份打错了,直接告诉我正确的年份就行。",
|
||||
lastSuccessfulCompareRange: "这是按上一次成功比较给出的范围。",
|
||||
@@ -516,6 +522,39 @@ export function rangeChangedAfterEvidence(
|
||||
return `范围从 ${fromText} 变为 ${toText}。`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Issue #148: say plainly that the new event did not tell the candidates
|
||||
* apart. Only when the server rescored and the range is unchanged; it never
|
||||
* invites more events (BUG-1084).
|
||||
*/
|
||||
export function withEvidenceNoSplit(
|
||||
body: string,
|
||||
from: readonly [string, string] | null | undefined,
|
||||
to: readonly [string, string] | null | undefined,
|
||||
): string {
|
||||
const fromText = formatClockRange(from);
|
||||
const toText = formatClockRange(to);
|
||||
if (!fromText || !toText || fromText !== toText) return body;
|
||||
const notice = RECTIFICATION_USER_COPY.evidenceNoSplit;
|
||||
const spoken = body.trim();
|
||||
if (!spoken) return notice;
|
||||
if (spoken.includes(notice)) return spoken;
|
||||
const prefix = /[。!?]$/.test(spoken) ? spoken : `${spoken}。`;
|
||||
return `${prefix}${notice}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Issue #148: how the reader's own time was given, said once in the opening.
|
||||
* Only for sources where the words are true; unknown or legacy get none.
|
||||
*/
|
||||
export function openingSourcePhrase(source: string | null | undefined): string | null {
|
||||
if (source === "hospital_record") return "按你的出生记录时间";
|
||||
if (source === "approximate") return "按你填的大概时间";
|
||||
if (source === "family_exact") return "按你填的时间";
|
||||
if (source === "period_only") return "按你给的时间段";
|
||||
return null;
|
||||
}
|
||||
|
||||
export function withRangeChangedAfterEvidence(
|
||||
body: string,
|
||||
from: readonly [string, string] | null | undefined,
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
openingSpokenBody,
|
||||
stripRangeProgressClaims,
|
||||
type OpeningBoundaryFacts,
|
||||
openingSourcePhrase,
|
||||
} from "../user-copy";
|
||||
import { resolveOpeningBoundaryFacts } from "../opening-boundary.ts";
|
||||
import { attemptTimeoutForRemainingBudget, canStartRetryAttempt } from "../../rectification-run-budget.ts";
|
||||
@@ -53,6 +54,7 @@ import {
|
||||
batchResultFromToolChunk,
|
||||
batchHasFutureDatedItem,
|
||||
batchRescoreFailed,
|
||||
batchRescoredWithoutDelivery,
|
||||
acceptedRecapLines,
|
||||
composeHostFallbackNarration,
|
||||
lastCompletedPublicTool,
|
||||
@@ -640,8 +642,11 @@ export async function streamV9Attempt(
|
||||
const openingRange = openingRangeFromCandidateRange(latestDossier.case.candidateRange);
|
||||
const productDelivery = productDeliveryForDomain(latestDossier.case.rectificationDomain);
|
||||
const required = openingBoundarySentence(openingFacts);
|
||||
if (required || !isAcceptableOpeningBody(answerText, openingRange, productDelivery, openingFacts)) {
|
||||
answerText = openingSpokenBody(openingRange, productDelivery, openingFacts);
|
||||
// Issue #148: a known source is a required clause of the window sentence,
|
||||
// so the server writes the opening whenever it applies.
|
||||
const sourceRequired = Boolean(openingRange && openingSourcePhrase(dossier.case.birthTimeSource));
|
||||
if (required || sourceRequired || !isAcceptableOpeningBody(answerText, openingRange, productDelivery, openingFacts)) {
|
||||
answerText = openingSpokenBody(openingRange, productDelivery, openingFacts, dossier.case.birthTimeSource);
|
||||
}
|
||||
}
|
||||
const latestInference = previousInferenceFromReceipt(
|
||||
@@ -656,6 +661,7 @@ export async function streamV9Attempt(
|
||||
futureDated: batchHasFutureDatedItem(batchToolResult),
|
||||
rangeBefore: rangeBeforeCompare,
|
||||
rangeAfter: rangeAfterEvidence,
|
||||
evidenceRescored: batchRescoredWithoutDelivery(batchToolResult),
|
||||
};
|
||||
// P3: the only clocks and percentages a model sentence may carry are this
|
||||
// turn's server facts (range, representative minute, event fit rate).
|
||||
|
||||
@@ -12,6 +12,7 @@ import type { SpokenFactWhitelist } from "./spoken-grounding";
|
||||
import {
|
||||
RECTIFICATION_USER_COPY,
|
||||
withCompareFailedRetryNotice,
|
||||
withEvidenceNoSplit,
|
||||
withRangeChangedAfterEvidence,
|
||||
withRescoreSkippedNotice,
|
||||
} from "../user-copy";
|
||||
@@ -56,6 +57,8 @@ export type TurnServerFacts = Readonly<{
|
||||
futureDated?: boolean;
|
||||
rangeBefore: readonly [string, string] | null;
|
||||
rangeAfter: readonly [string, string] | null;
|
||||
/** Issue #148: events were recorded and rescored this turn (not the delivery turn). */
|
||||
evidenceRescored?: boolean;
|
||||
}>;
|
||||
|
||||
/** Join the server fact sentences after an already trimmed and whitelisted body. */
|
||||
@@ -69,7 +72,10 @@ export function composeSpokenWithServerFacts(
|
||||
const withFuture = (text: string) => !future || text.includes(future) ? text : text ? `${text}${/[。!?]$/.test(text) ? "" : "。"}${future}` : future;
|
||||
if (facts.compareFailed) spoken = withCompareFailedRetryNotice(spoken);
|
||||
if (facts.rescoreSkipped) return withFuture(withRescoreSkippedNotice(spoken));
|
||||
return withFuture(withRangeChangedAfterEvidence(spoken, facts.rangeBefore, facts.rangeAfter));
|
||||
const changed = withRangeChangedAfterEvidence(spoken, facts.rangeBefore, facts.rangeAfter);
|
||||
return withFuture(facts.evidenceRescored === true
|
||||
? withEvidenceNoSplit(changed, facts.rangeBefore, facts.rangeAfter)
|
||||
: changed);
|
||||
}
|
||||
|
||||
export const MAX_ATTEMPTS = 2;
|
||||
|
||||
@@ -128,6 +128,23 @@ export function batchHasFutureDatedItem(batchResult: unknown): boolean {
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Issue #148: the batch recorded at least one event, the rescore ran to
|
||||
* completion, and this is not the delivery turn. Only then may an unchanged
|
||||
* range be spoken as 「分不出前后」.
|
||||
*/
|
||||
export function batchRescoredWithoutDelivery(batchResult: unknown): boolean {
|
||||
if (!batchResult || typeof batchResult !== "object" || Array.isArray(batchResult)) return false;
|
||||
const row = batchResult as {
|
||||
accepted_count?: unknown;
|
||||
rescore?: { status?: unknown };
|
||||
range_after_rescore?: { delivers_range_this_turn?: unknown } | null;
|
||||
};
|
||||
return typeof row.accepted_count === "number" && row.accepted_count > 0
|
||||
&& row.rescore?.status === "completed"
|
||||
&& row.range_after_rescore?.delivers_range_this_turn !== true;
|
||||
}
|
||||
|
||||
export function batchRescoreFailed(batchResult: unknown): boolean {
|
||||
if (!batchResult || typeof batchResult !== "object" || Array.isArray(batchResult)) return false;
|
||||
const rescore = (batchResult as { rescore?: { status?: unknown } }).rescore;
|
||||
|
||||
@@ -118,9 +118,8 @@ export function deriveRectificationChatView(input: RectificationChatViewInput) {
|
||||
candidateTimes: candidateResult?.candidates.map((candidate) => candidate.time) ?? [],
|
||||
inferenceMarks: candidateResult?.inferenceMarks ?? [],
|
||||
stage: caseStage,
|
||||
// The same call the board's title-row clock makes, on the same result, so
|
||||
// the bar and `.rectification-board__clock` are one number by construction
|
||||
// rather than by two code paths that happen to agree today.
|
||||
// The one place the working minute is shown (issue #146 took it off the
|
||||
// board's title row). The board's peek label reads the same call.
|
||||
workingTime: workingRectificationTime(candidateResult),
|
||||
workingAdopted: Boolean(candidateResult?.selectedTime),
|
||||
segmentSummary: candidateResult?.rangeDelivery?.segment_summary,
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* 「我说错了,改一条」 on the result card (issue #147). The list is the case's
|
||||
* confirmed events, worded with the same recap line the conversation already
|
||||
* shows. Picking one only fills the composer; the user finishes the sentence
|
||||
* and sends it, and the agent revises that event with its existing tool. No
|
||||
* new route, nothing sent or charged by the pick itself.
|
||||
*/
|
||||
|
||||
import { displayDateLabel, eventPhraseFromSummary } from "./rectification-agentic/v9/evidence-model.ts";
|
||||
import { evidenceRecapLine } from "./rectification-agentic/v9/host-fallback.ts";
|
||||
|
||||
export const RECTIFICATION_FIX_COPY = {
|
||||
fixOne: "我说错了,改一条",
|
||||
pickHint: "选一条要改的,输入框里会先写好开头:",
|
||||
} as const;
|
||||
|
||||
/** Confirmed events only, oldest first as stored, each once. */
|
||||
export function fixableEvidenceLines(value: unknown): string[] {
|
||||
if (!Array.isArray(value)) return [];
|
||||
const lines: string[] = [];
|
||||
for (const item of value) {
|
||||
if (!item || typeof item !== "object") continue;
|
||||
const row = item as Record<string, unknown>;
|
||||
if (row.status !== "confirmed") continue;
|
||||
const summary = typeof row.summary === "string" ? row.summary : "";
|
||||
if (!summary.trim()) continue;
|
||||
const precision = typeof row.datePrecision === "string" ? row.datePrecision : "unknown";
|
||||
const from = typeof row.occurredFrom === "string" ? row.occurredFrom : null;
|
||||
const to = typeof row.occurredTo === "string" ? row.occurredTo : null;
|
||||
const line = evidenceRecapLine({
|
||||
display_date_label: displayDateLabel(precision, from, to),
|
||||
event_phrase: eventPhraseFromSummary(summary),
|
||||
}).trim();
|
||||
if (line && !lines.includes(line)) lines.push(line);
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
/** The composer's opening words; the user writes what it should be. */
|
||||
export function fixEvidenceDraft(line: string): string {
|
||||
return `我说错了:「${line}」应该是`;
|
||||
}
|
||||
|
||||
/** The primary adopt label on the most likely column. */
|
||||
export function adoptAndOpenChartLabel(time: string): string {
|
||||
return `采用 ${time} 并打开星盘`;
|
||||
}
|
||||
|
||||
export const RECTIFICATION_OTHER_CANDIDATES_LABEL = "还不能排除";
|
||||
export const RECTIFICATION_SWITCH_TO_THIS_LABEL = "改用这个";
|
||||
export const RECTIFICATION_MOST_LIKELY_LABEL = "最可能的出生时间";
|
||||
@@ -112,6 +112,8 @@ export type RectificationCaseSnapshotPayload = Readonly<{
|
||||
birth_time_source?: unknown;
|
||||
}>;
|
||||
turns?: unknown;
|
||||
/** The case's evidence rows; the result card lists the confirmed ones (issue #147). */
|
||||
evidence?: unknown;
|
||||
}>;
|
||||
|
||||
/** `candidate_range` as `[start, end]`; null when absent or malformed. */
|
||||
|
||||
@@ -635,7 +635,10 @@ test("compact board overlays chat as a bottom sheet above the composer", () => {
|
||||
assert.match(styles, /\.rectification-board__close \{[\s\S]*width: 44px/);
|
||||
assert.match(styles, /\.rectification-board__close \{[\s\S]*min-height: 44px/);
|
||||
assert.match(board, /<X aria-hidden="true"/);
|
||||
assert.match(chat, /compactBoard && !boardOpen \? \(/);
|
||||
// 原值:只在手机且盘面没打开时出「看盘面」入口(compactBoard && !boardOpen)。
|
||||
// 新值:桌面上也出,手机上盘面打开时才收起。
|
||||
// 原因:issue #146(看盘面按钮在所有宽度都有,盘面默认关着,代表分钟只在时间轴上写一次)。
|
||||
assert.match(chat, /!compactBoard \|\| !boardOpen \? \(/);
|
||||
assert.match(chat, /headerSlot && boardPeek \? createPortal\(boardPeek, headerSlot\)/);
|
||||
assert.doesNotMatch(chat, /querySelector\(/);
|
||||
assert.doesNotMatch(chat, /setHeaderSlot/);
|
||||
|
||||
@@ -111,8 +111,11 @@ const diagnosticReport = JSON.parse(readFileSync(
|
||||
// 原值(10):docs/research/sealed_holdout_rerun_scoring_perf_2026_10_13.json
|
||||
// 新值(10):docs/research/sealed_holdout_rerun_upstream_sync8_2026_10_18.json
|
||||
// 原因(10):sync8(BUG-1380~1390)改了冻结身份文件,输出不变,ERR-110 另存新记录。确认门行为与阈值不变。
|
||||
// 原值(11):docs/research/sealed_holdout_rerun_upstream_sync8_2026_10_18.json
|
||||
// 新值(11):docs/research/sealed_holdout_rerun_upstream_sync9b_2026_10_28.json
|
||||
// 原因(11):sync9b(BUG-1494~1495)改了冻结身份里的 scripts/varga.py,输出不变,ERR-110 另存新记录。确认门行为与阈值不变。
|
||||
const currentTreeRerun = JSON.parse(readFileSync(
|
||||
new URL("../../docs/research/sealed_holdout_rerun_upstream_sync8_2026_10_18.json", import.meta.url),
|
||||
new URL("../../docs/research/sealed_holdout_rerun_upstream_sync9b_2026_10_28.json", import.meta.url),
|
||||
"utf8",
|
||||
)) as {
|
||||
frozen_record: { implementation_sha256: string };
|
||||
@@ -260,9 +263,10 @@ test("sealed holdout aggregates match the v3 report and still block confirmation
|
||||
// 原值(7):…upstream_sync6_2026_10_07.json / 新值(7):…ephemeris_nutation_2026_10_08.json / 原因(7):章动修正与 scoring-12 重新冻结(ERR-110,BUG-1275)。
|
||||
// 原值(8):…ephemeris_nutation_2026_10_08.json / 新值(8):…scoring_perf_2026_10_13.json / 原因(8):BUG-1323~1325 改了冻结身份文件,输出不变,ERR-110 另存新记录。
|
||||
// 原值(9):…scoring_perf_2026_10_13.json / 新值(9):…upstream_sync8_2026_10_18.json / 原因(9):sync8 改了冻结身份文件,输出不变,ERR-110 另存新记录。
|
||||
// 原值(10):…upstream_sync8_2026_10_18.json / 新值(10):…upstream_sync9b_2026_10_28.json / 原因(10):sync9b 改了冻结身份里的 varga.py,输出不变,ERR-110 另存新记录。
|
||||
assert.equal(
|
||||
productHoldout.current_tree_scorer.source_report,
|
||||
"docs/research/sealed_holdout_rerun_upstream_sync8_2026_10_18.json",
|
||||
"docs/research/sealed_holdout_rerun_upstream_sync9b_2026_10_28.json",
|
||||
);
|
||||
assert.equal(currentTreeRerun.official_valid_independent_blind, false);
|
||||
assert.equal(productHoldout.current_tree_scorer.matches_metrics_scorer, false);
|
||||
|
||||
@@ -553,7 +553,8 @@ test("the minute card hides the adopt button and the chart-clock line when adopt
|
||||
const allowed = renderRange(cardResult("adopt_representative", true, minuteDelivery()));
|
||||
assert.match(allowed, /<button/);
|
||||
assert.match(allowed, /采用后,排盘使用这一日期和时间/);
|
||||
assert.match(allowed, /更像这个/);
|
||||
// 原值:/更像这个/。新值:区分得开时最可能那列是「采用 HH:MM 并打开星盘」。原因:issue #147。
|
||||
assert.match(allowed, /更像这个|采用 \d{2}:\d{2} 并打开星盘/);
|
||||
});
|
||||
|
||||
test("a refused adoption refreshes the snapshot and says so in Chinese", async () => {
|
||||
@@ -734,3 +735,21 @@ test("answering widen A with a failed rescore never delivers or adopts on the ol
|
||||
assert.notEqual(receipt.nextAction.type, "ready_to_adopt");
|
||||
assert.notEqual(receipt.nextAction.type, "complete_with_range");
|
||||
});
|
||||
|
||||
// Issue #147: the segment card answers "which chart" (BUG-1151), so it gets the
|
||||
// actions row but never a minute lead, and keeps its own adopt label.
|
||||
test("the segment card gets 改一条 but no minute lead and no 再补一件事", () => {
|
||||
const html = renderToStaticMarkup(createElement(RectificationRangeDelivery, {
|
||||
result: cardResult("adopt_representative", true, segmentDelivery()),
|
||||
acceptingCandidateId: null,
|
||||
readonly: false,
|
||||
onAccept: () => {},
|
||||
fixableEvidence: ["2016-09 升学"],
|
||||
onFixEvidence() {},
|
||||
}));
|
||||
assert.match(html, /用哪张盘解读/);
|
||||
assert.doesNotMatch(html, /再补一件事/, "BUG-1084: the card does not invite more events");
|
||||
assert.match(html, /我说错了,改一条/);
|
||||
assert.doesNotMatch(html, /最可能|rectification-result-lead|并打开星盘/);
|
||||
assert.match(html, /盘解读<\/button>/);
|
||||
});
|
||||
|
||||
@@ -146,10 +146,17 @@ test("D4 threshold is 5 points between the first and second column", () => {
|
||||
});
|
||||
|
||||
test("D3 range is the title and the representative minute is the subtitle", () => {
|
||||
// 原值:前两列差距 ≥ 5 时仍以「目前范围 …」为 <strong> 卡头,「最可能 HH:MM」作副标题。
|
||||
// 新值:区分得开时卡头改为「最可能的出生时间 HH:MM」,范围降为下一行;区分不开时仍以范围为卡头(D3 只在这时成立)。
|
||||
// 原因:issue #147(产品 2026-10-28 同意:区分得开才以最可能时刻为卡头)。
|
||||
const html = render([0.45, 0.3, 0.25]);
|
||||
assert.match(html, /<strong>目前范围 05:00–05:15(对照了 2 件经历)<\/strong>/);
|
||||
assert.match(html, /rectification-range-delivery__most-likely">最可能 05:07</);
|
||||
assert.ok(html.indexOf("最可能 05:07") < html.indexOf("rectification-range-delivery__columns"));
|
||||
assert.match(html, /rectification-result-lead__time">05:07</);
|
||||
assert.match(html, /rectification-range-delivery__range-line">目前范围 05:00–05:15(对照了 2 件经历)</);
|
||||
assert.ok(html.indexOf("rectification-result-lead") < html.indexOf("rectification-range-delivery__range-line"));
|
||||
assert.ok(html.indexOf("rectification-range-delivery__range-line") < html.indexOf("rectification-range-delivery__columns"));
|
||||
const flat = render([0.28, 0.26, 0.24]);
|
||||
assert.match(flat, /<strong>目前范围 05:00–05:15(对照了 2 件经历)<\/strong>/);
|
||||
assert.doesNotMatch(flat, /rectification-result-lead/);
|
||||
assert.equal(rangeDeliveryMostLikely("05:07"), "最可能 05:07");
|
||||
});
|
||||
|
||||
@@ -315,3 +322,30 @@ test("D2 a delivered card does not carry an unasked guided window as the next qu
|
||||
const collecting = buildMethodFollowupPlan({ ...base, sessionOutcome: "collect_evidence" });
|
||||
assert.doesNotMatch(collecting.next_followup?.collection_key ?? "", /^collect:guided:window:/);
|
||||
});
|
||||
|
||||
// Issue #147: a distinct result leads with the most likely minute; its column
|
||||
// adopts and opens the chart, the others stay adoptable as 「改用这个」.
|
||||
test("a distinct result puts the most likely column first with 采用并打开星盘 and keeps the others adoptable", () => {
|
||||
const calls: Array<[string, boolean | undefined]> = [];
|
||||
const html = renderToStaticMarkup(createElement(RectificationRangeDelivery, {
|
||||
result: result([0.45, 0.3, 0.25]),
|
||||
acceptingCandidateId: null,
|
||||
readonly: false,
|
||||
onAccept: (candidateId: string, openChart?: boolean) => { calls.push([candidateId, openChart]); },
|
||||
fixableEvidence: ["2016-09 升学"],
|
||||
onFixEvidence() {},
|
||||
}));
|
||||
assert.match(html, /采用 05:07 并打开星盘/);
|
||||
assert.equal([...html.matchAll(/改用这个/g)].length, 2);
|
||||
assert.match(html, /还不能排除/);
|
||||
assert.ok(html.indexOf("采用 05:07 并打开星盘") < html.indexOf("还不能排除"));
|
||||
assert.doesNotMatch(html, /再补一件事/, "BUG-1084: the card does not invite more events");
|
||||
assert.match(html, /我说错了,改一条/);
|
||||
assert.doesNotMatch(html, /把握/, "no confidence label (issue #147, BUG-560)");
|
||||
});
|
||||
|
||||
test("an indistinct result keeps 更像这个 on every column and no lead", () => {
|
||||
const html = render([0.28, 0.26, 0.24]);
|
||||
assert.doesNotMatch(html, /并打开星盘|改用这个|还不能排除|rectification-result-lead/);
|
||||
assert.match(html, /更像这个/);
|
||||
});
|
||||
|
||||
@@ -58,7 +58,10 @@ test("the bar's working minute is the board clock's own value, not a second deri
|
||||
// passes the result of that same call into the bar, so the two surfaces
|
||||
// cannot drift apart — there is one expression, not two that agree today.
|
||||
assert.match(board, /const workingTime = workingRectificationTime\(result\);/);
|
||||
assert.match(board, /className="rectification-board__clock">\{workingTime\}/);
|
||||
// 原值:盘面标题行渲染 rectification-board__clock,与时间轴同一个值。
|
||||
// 新值:盘面不再写时刻,代表分钟只在时间轴上出现一次。
|
||||
// 原因:issue #146(看盘面按钮在所有宽度都有,盘面默认关着,代表分钟只在时间轴上写一次)。
|
||||
assert.doesNotMatch(board, /className="rectification-board__clock"/);
|
||||
assert.match(chat, /workingTime: workingRectificationTime\(candidateResult\)/);
|
||||
|
||||
const result = candidateSnapshot();
|
||||
|
||||
@@ -395,7 +395,8 @@ test("range-delivery DOM is one button per row and adopted is aria-pressed", ()
|
||||
assert.doesNotMatch(html, /说不好/);
|
||||
assert.doesNotMatch(html, /排盘用/);
|
||||
const source = readFileSync(new URL("../src/components/rectification-range-delivery.tsx", import.meta.url), "utf8");
|
||||
assert.match(source, /onAccept\(column\.candidate_id\)/);
|
||||
// 原值:onAccept(column.candidate_id)。新值:带第二个参数,最可能那列(且无记录冲突)采用后打开星盘。原因:issue #147。
|
||||
assert.match(source, /onAccept\(column\.candidate_id, isPrimary && !recordConflict\)/);
|
||||
assert.match(source, /aria-pressed=\{adopted\}/);
|
||||
});
|
||||
|
||||
|
||||
@@ -191,7 +191,13 @@ test("non-conflict states keep 更像这个 and show no switch note", () => {
|
||||
[RECORD_OUTSIDE, "period_only"],
|
||||
] as const) {
|
||||
const markup = html(reported, source);
|
||||
assert.ok(markup.includes(RECTIFICATION_USER_COPY.rangeDeliveryMoreLikeThis), `${reported}/${source}`);
|
||||
// 原值:每列都是「更像这个」。
|
||||
// 新值:区分得开时最可能那列是「采用 HH:MM 并打开星盘」、其余「改用这个」;区分不开时仍是「更像这个」。
|
||||
// 原因:issue #147。无冲突时仍然不出现「改用校正结果」和换时间小字。
|
||||
assert.ok(
|
||||
markup.includes(RECTIFICATION_USER_COPY.rangeDeliveryMoreLikeThis) || /采用 \d{2}:\d{2} 并打开星盘/.test(markup),
|
||||
`${reported}/${source}`,
|
||||
);
|
||||
assert.equal(markup.includes(SWITCH_TO_RECTIFIED_LABEL), false, `${reported}/${source}`);
|
||||
assert.equal(markup.includes("换成 04:53"), false, `${reported}/${source}`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
import { createElement } from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
|
||||
import { RECTIFICATION_BOARD_OPEN_LABEL, RectificationBoard, RectificationBoardPeek } from "../src/components/rectification-board.tsx";
|
||||
import { RectificationResultActions, RectificationResultLead } from "../src/components/rectification-result-parts.tsx";
|
||||
import { diffRectificationBoard } from "../src/lib/rectification-board-model.ts";
|
||||
import {
|
||||
RECTIFICATION_FIX_COPY,
|
||||
adoptAndOpenChartLabel,
|
||||
fixEvidenceDraft,
|
||||
fixableEvidenceLines,
|
||||
} from "../src/lib/rectification-fix-evidence.ts";
|
||||
|
||||
// Issues #146 and #147: one working minute on the timeline, the board behind
|
||||
// 「看盘面」, and the result card's lead and actions. Fictional data only.
|
||||
const read = (path: string) => readFileSync(new URL(path, import.meta.url), "utf8");
|
||||
|
||||
test("only confirmed events are listed to fix, once each, in the conversation's recap wording", () => {
|
||||
const lines = fixableEvidenceLines([
|
||||
{ status: "confirmed", summary: "升学", datePrecision: "month", occurredFrom: "2016-09-01", occurredTo: null },
|
||||
{ status: "confirmed", summary: "升学", datePrecision: "month", occurredFrom: "2016-09-01", occurredTo: null },
|
||||
{ status: "pending_confirmation", summary: "搬家", datePrecision: "month", occurredFrom: "2023-08-01", occurredTo: null },
|
||||
{ status: "superseded", summary: "入职", datePrecision: "day", occurredFrom: "2024-04-07", occurredTo: null },
|
||||
{ status: "confirmed", summary: "入职", datePrecision: "day", occurredFrom: "2024-04-07", occurredTo: null },
|
||||
{ status: "confirmed", summary: " ", datePrecision: "day", occurredFrom: "2024-04-07", occurredTo: null },
|
||||
null,
|
||||
"x",
|
||||
]);
|
||||
assert.equal(lines.length, 2);
|
||||
assert.match(lines[0]!, /2016-09/);
|
||||
assert.match(lines[1]!, /2024-04-07/);
|
||||
assert.deepEqual(fixableEvidenceLines(undefined), []);
|
||||
assert.equal(fixEvidenceDraft("2016-09 升学"), "我说错了:「2016-09 升学」应该是");
|
||||
assert.equal(adoptAndOpenChartLabel("07:24"), "采用 07:24 并打开星盘");
|
||||
});
|
||||
|
||||
test("the actions row offers only 改一条, and only with a callback and a confirmed event", () => {
|
||||
assert.equal(renderToStaticMarkup(createElement(RectificationResultActions, { evidence: ["2016-09 升学"], disabled: false })), "");
|
||||
const html = renderToStaticMarkup(createElement(RectificationResultActions, {
|
||||
evidence: ["2016-09 升学"],
|
||||
disabled: false,
|
||||
onFixEvidence() {},
|
||||
}));
|
||||
assert.match(html, new RegExp(RECTIFICATION_FIX_COPY.fixOne));
|
||||
assert.doesNotMatch(html, /再补一件事/, "BUG-1084: no invitation to add more events");
|
||||
assert.match(html, /aria-expanded="false"/);
|
||||
assert.doesNotMatch(html, /2016-09 升学/, "the event list opens on click");
|
||||
const none = renderToStaticMarkup(createElement(RectificationResultActions, {
|
||||
evidence: [],
|
||||
disabled: false,
|
||||
onFixEvidence() {},
|
||||
}));
|
||||
assert.equal(none, "", "nothing to fix without a confirmed event");
|
||||
});
|
||||
|
||||
test("the lead shows the minute and the Lagna when known", () => {
|
||||
assert.match(renderToStaticMarkup(createElement(RectificationResultLead, { time: "07:24", lagna: "巨蟹座" })), /最可能的出生时间[\s\S]*07:24[\s\S]*上升巨蟹座/);
|
||||
assert.doesNotMatch(renderToStaticMarkup(createElement(RectificationResultLead, { time: "07:24", lagna: null })), /上升/);
|
||||
});
|
||||
|
||||
test("the board is closed by default at every width and has no clock in its title row", () => {
|
||||
const props = {
|
||||
result: null,
|
||||
declaredTime: "05:10",
|
||||
savedStatus: null,
|
||||
diff: diffRectificationBoard(null, null),
|
||||
boardId: "board",
|
||||
titleId: "title",
|
||||
onClose() {},
|
||||
};
|
||||
assert.equal(renderToStaticMarkup(createElement(RectificationBoard, { ...props, compact: false, open: false })), "");
|
||||
assert.equal(renderToStaticMarkup(createElement(RectificationBoard, { ...props, compact: true, open: false })), "");
|
||||
const open = renderToStaticMarkup(createElement(RectificationBoard, { ...props, compact: false, open: true }));
|
||||
assert.match(open, /当前盘面/);
|
||||
assert.doesNotMatch(open, /rectification-board__clock/);
|
||||
assert.match(open, /aria-label="关闭盘面"/, "the desktop column closes too");
|
||||
});
|
||||
|
||||
test("看盘面 says what it does; the time stays in its accessible name only", () => {
|
||||
const html = renderToStaticMarkup(createElement(RectificationBoardPeek, {
|
||||
result: null,
|
||||
declaredTime: "05:10",
|
||||
expanded: false,
|
||||
boardId: "board",
|
||||
onOpen() {},
|
||||
}));
|
||||
assert.equal(RECTIFICATION_BOARD_OPEN_LABEL, "看盘面");
|
||||
assert.match(html, /<strong>看盘面<\/strong>/);
|
||||
assert.doesNotMatch(html, /<span>/);
|
||||
assert.match(html, /aria-label="看盘面,[^"]*05:10"/);
|
||||
});
|
||||
|
||||
test("adopting from the most likely column opens the chart only after the server saved it", () => {
|
||||
const hook = read("../src/hooks/use-open-chart-after-adopt.ts");
|
||||
assert.match(hook, /onSaved\?\.\(time, status\);\s*if \(!openAfterSave\.current\) return;\s*openAfterSave\.current = false;\s*navigateInApp\("\/chart"\);/);
|
||||
const chat = read("../src/components/rectification-agentic-chat.tsx");
|
||||
assert.match(chat, /armChartAfterAdopt\(openChart\);/);
|
||||
assert.match(chat, /onSaved: savedThenChart,/);
|
||||
assert.match(chat, /onFixEvidence=\{\(line\) => \{\s*setDraft\(fixEvidenceDraft\(line\)\);/);
|
||||
const segment = read("../src/components/rectification-segment-delivery.tsx");
|
||||
assert.doesNotMatch(segment, /RectificationResultLead/, "the segment product never leads with a minute (BUG-1151)");
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
OPENING_BODY_JARGON,
|
||||
RECTIFICATION_USER_COPY,
|
||||
isAcceptableOpeningBody,
|
||||
openingSourcePhrase,
|
||||
openingSpokenBody,
|
||||
withEvidenceNoSplit,
|
||||
} from "../src/lib/rectification-agentic/user-copy.ts";
|
||||
import { composeSpokenWithServerFacts } from "../src/lib/rectification-agentic/v9/agent-run-support.ts";
|
||||
import { batchRescoredWithoutDelivery } from "../src/lib/rectification-agentic/v9/host-fallback.ts";
|
||||
import { productDeliveryForDomain } from "../src/lib/rectification-agentic/v9/product-delivery.ts";
|
||||
|
||||
// Issue #148 (product 2026-10-28): the opening's window sentence says where the
|
||||
// reader's time came from, and an event that left the range unchanged is said
|
||||
// to have not told the candidates apart. No invitation to add more (BUG-1084).
|
||||
const RANGE = ["07:00", "08:00"] as const;
|
||||
|
||||
test("the opening names the time source inside the window sentence, only where the words are true", () => {
|
||||
assert.equal(openingSourcePhrase("hospital_record"), "按你的出生记录时间");
|
||||
assert.equal(openingSourcePhrase("approximate"), "按你填的大概时间");
|
||||
assert.equal(openingSourcePhrase("family_exact"), "按你填的时间");
|
||||
assert.equal(openingSourcePhrase("period_only"), "按你给的时间段");
|
||||
for (const source of ["unknown", "legacy_import", "", null, undefined]) {
|
||||
assert.equal(openingSourcePhrase(source), null, String(source));
|
||||
}
|
||||
const plain = openingSpokenBody(RANGE);
|
||||
const sourced = openingSpokenBody(RANGE, null, null, "approximate");
|
||||
assert.match(sourced, /我们来把你的出生时间缩小到更准的范围,按你填的大概时间,现在先在 07:00–08:00 之间找。/);
|
||||
assert.equal(sourced.split("。").length, plain.split("。").length, "same sentence count: still three plain layers");
|
||||
assert.equal(openingSpokenBody(RANGE, null, null, "unknown"), plain);
|
||||
assert.equal(openingSpokenBody(null, null, null, "approximate"), openingSpokenBody(null), "no window, no source clause");
|
||||
for (const word of OPENING_BODY_JARGON) assert.equal(sourced.includes(word), false, word);
|
||||
assert.equal(isAcceptableOpeningBody(sourced, RANGE), true);
|
||||
});
|
||||
|
||||
test("the segment product's opening carries the source before the check window", () => {
|
||||
const product = productDeliveryForDomain("career");
|
||||
const body = openingSpokenBody(RANGE, product, null, "hospital_record");
|
||||
assert.match(body, /能不能选定用来解读的星盘,按你的出生记录时间,核对窗口是 07:00–08:00。/);
|
||||
assert.equal(isAcceptableOpeningBody(body, RANGE, product), true);
|
||||
});
|
||||
|
||||
test("an event that left the range unchanged gets one 分不出前后 sentence, and nothing else changes", () => {
|
||||
const notice = RECTIFICATION_USER_COPY.evidenceNoSplit;
|
||||
assert.doesNotMatch(notice, /再补|再说一件|补一件/, "BUG-1084: never invite more events");
|
||||
const same = composeSpokenWithServerFacts("记下了。", { compareFailed: false, rescoreSkipped: false, rangeBefore: RANGE, rangeAfter: RANGE, evidenceRescored: true });
|
||||
assert.equal(same, `记下了。${notice}`);
|
||||
const moved = composeSpokenWithServerFacts("记下了。", { compareFailed: false, rescoreSkipped: false, rangeBefore: RANGE, rangeAfter: ["07:10", "07:50"], evidenceRescored: true });
|
||||
assert.doesNotMatch(moved, new RegExp(notice));
|
||||
assert.match(moved, /范围从 07:00–08:00 变为 07:10–07:50。/);
|
||||
const noEvent = composeSpokenWithServerFacts("好。", { compareFailed: false, rescoreSkipped: false, rangeBefore: RANGE, rangeAfter: RANGE });
|
||||
assert.equal(noEvent, "好。");
|
||||
const skipped = composeSpokenWithServerFacts("记下了。", { compareFailed: false, rescoreSkipped: true, rangeBefore: RANGE, rangeAfter: RANGE, evidenceRescored: true });
|
||||
assert.doesNotMatch(skipped, new RegExp(notice), "no rescore, no claim about the range");
|
||||
assert.equal(withEvidenceNoSplit(`记下了。${notice}`, RANGE, RANGE), `记下了。${notice}`, "said once");
|
||||
assert.equal(withEvidenceNoSplit("记下了。", null, RANGE), "记下了。");
|
||||
});
|
||||
|
||||
test("only a completed rescore of accepted events outside the delivery turn counts", () => {
|
||||
const ok = { accepted_count: 1, rescore: { status: "completed" }, range_after_rescore: { delivers_range_this_turn: false } };
|
||||
assert.equal(batchRescoredWithoutDelivery(ok), true);
|
||||
assert.equal(batchRescoredWithoutDelivery({ ...ok, accepted_count: 0 }), false);
|
||||
assert.equal(batchRescoredWithoutDelivery({ ...ok, rescore: { status: "skipped" } }), false);
|
||||
assert.equal(batchRescoredWithoutDelivery({ ...ok, rescore: { status: "failed" } }), false);
|
||||
assert.equal(batchRescoredWithoutDelivery({ ...ok, range_after_rescore: { delivers_range_this_turn: true } }), false);
|
||||
assert.equal(batchRescoredWithoutDelivery(null), false);
|
||||
assert.equal(batchRescoredWithoutDelivery([]), false);
|
||||
});
|
||||
|
||||
test("the attempt wires the source into the opening and the rescore flag into the server facts", () => {
|
||||
const attempt = readFileSync(new URL("../src/lib/rectification-agentic/v9/agent-run-attempt.ts", import.meta.url), "utf8");
|
||||
assert.match(attempt, /const sourceRequired = Boolean\(openingRange && openingSourcePhrase\(dossier\.case\.birthTimeSource\)\);/);
|
||||
assert.match(attempt, /openingSpokenBody\(openingRange, productDelivery, openingFacts, dossier\.case\.birthTimeSource\)/);
|
||||
assert.match(attempt, /evidenceRescored: batchRescoredWithoutDelivery\(batchToolResult\),/);
|
||||
});
|
||||
@@ -226,7 +226,9 @@ test("the choice card confirms the tap and the board's first state shows the dec
|
||||
assert.match(board, /rectificationBoardPeekCopy\(result, declaredTime, birthTimeSource\)/);
|
||||
assert.doesNotMatch(board, /补充经历后,这里会显示当前本命宫位和换升时刻/);
|
||||
assert.match(chat, /\$\{candidateResult \? "" : " is-board-empty"\}/);
|
||||
assert.match(styles, /\.rectification-workspace\.is-board-empty \{\s*grid-template-columns: minmax\(0, 1fr\) minmax\(16rem, 18rem\);/);
|
||||
// The empty modifier is declared before `.is-compact`, so the single-column layout still wins.
|
||||
assert.ok(styles.indexOf(".rectification-workspace.is-board-empty {") < styles.indexOf(".rectification-workspace.is-compact {"));
|
||||
// 原值:.rectification-workspace.is-board-empty 常驻两栏(右栏 16–18rem)。
|
||||
// 新值:只有盘面打开且不是手机布局时才分出右栏,空盘面仍用窄的那档。
|
||||
// 原因:issue #146(看盘面按钮在所有宽度都有,盘面默认关着,代表分钟只在时间轴上写一次)。
|
||||
assert.match(styles, /\.rectification-workspace\.is-board-empty\.is-board-open:not\(\.is-compact\) \{\s*grid-template-columns: minmax\(0, 1fr\) minmax\(16rem, 18rem\);/);
|
||||
assert.match(styles, /\.rectification-workspace \{[^}]*grid-template-columns: minmax\(0, 1fr\);/);
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"dataset_path": "references/real_case_calibration/minute_rectification_holdout_v3.json",
|
||||
"report_path": "references/real_case_calibration/minute_rectification_holdout_v3_report.json",
|
||||
"source_audit_status": "corrected_known_date_errors",
|
||||
"evaluated_on": "2026-10-18",
|
||||
"evaluated_on": "2026-10-28",
|
||||
"evaluated_on_scope": "latest_fixed_protocol_rerun_not_historical_runtime_metrics",
|
||||
"historical_source_audit_status": "invalidated_after_replay",
|
||||
"report_status": "invalidated_after_source_audit",
|
||||
@@ -24,7 +24,7 @@
|
||||
"evaluated_on": "2026-07-21"
|
||||
},
|
||||
"current_tree_scorer": {
|
||||
"implementation_sha256": "bf8981b5c41702f9b333444deee2ab7378b9e98b111240e85ea62e6aae02fda8",
|
||||
"implementation_sha256": "b44505aa0034e486fcfc29c8611a9559333a97c1005f92e6a5356bd855122205",
|
||||
"extended_identity": {
|
||||
"historical_artifacts_manifest_sha256": "6102a26a840be207b5858b3a4c0509274468ae9071e86d308cbbac7371d6864e",
|
||||
"production_scoring_files": [
|
||||
@@ -52,7 +52,7 @@
|
||||
"scripts/shadbala.py",
|
||||
"scripts/varga.py"
|
||||
],
|
||||
"production_scoring_sha256": "c2c23bcbde5de58e519ae1d6545620edd0f738ce8dc2e872e659b598137a728f",
|
||||
"production_scoring_sha256": "7a727418651321d1b9073588587050c4dc6292587748cf35734dd76cefb0effe",
|
||||
"research_files": [
|
||||
"scripts/research/reported_offset_sweep.py",
|
||||
"scripts/research/cluster_width_lib.py",
|
||||
@@ -62,7 +62,7 @@
|
||||
"scripts/minute_rectification_fact_blind_eval_v4.py",
|
||||
"scripts/minute_rectification_holdout_validator.py"
|
||||
],
|
||||
"research_implementation_sha256": "9df579bdacd7dec43abd92c9abc1831771feb800fc29e6864dc315d083d52a03",
|
||||
"research_implementation_sha256": "6fa0b0c335ac0cdc25ee887d432ae2f1f6487c665aed9665ad5cc0bc6ef0283b",
|
||||
"file_sha256": {
|
||||
"scripts/active_rectification_event_engine.py": "96c537d221a84680ddaaba74e3335162c44b5bbd9a60d10e69550a2095910d98",
|
||||
"scripts/active_rectification_events.py": "03e333ccf1103caf685dfe50fe890ab1793e945037085f50ce834a4a3b232222",
|
||||
@@ -90,17 +90,17 @@
|
||||
"scripts/rectification/scoring_service.py": "fa056fa30d9e6ffa4b75295fc49c793e35b26ebeca64ca843d26c8abef3050f2",
|
||||
"scripts/research/cluster_width_lib.py": "1d8f2ffa7795d62069deebb2de8e16749317037d4ed3f24a2c0092676e5cfcc6",
|
||||
"scripts/research/probe_supply_after_six.py": "098a5b4398d6f8d996917d809c2fc922ad64031d3b70660f8ef7c876aba92438",
|
||||
"scripts/research/reported_offset_sweep.py": "e10537ca2caba3a4e6a8d2460265543b0cca55935afa8004108e45bef4bc3de7",
|
||||
"scripts/research/sealed_holdout_rerun.py": "7945353014b93b6d7641f278779111d85b1773293781343b802a9b4b655ca4b5",
|
||||
"scripts/research/reported_offset_sweep.py": "8ecc381dfaa3c0e6bea058cfa44c606aabdb1086c137f16da82ee5a3bf5ccc91",
|
||||
"scripts/research/sealed_holdout_rerun.py": "ebcdae760f2b76c81d5d0c8e2bf347cff4c81cca9074f27ae94b6734408ccd42",
|
||||
"scripts/shadbala.py": "334dc9d8e4292f0c95d40fd4c29285aa89b9e49d4f187fb9498f4bb0ad93bdfc",
|
||||
"scripts/varga.py": "c1f06967c3d4d214edadff139c24aa31e59fb628477174235abe14f799a99076"
|
||||
"scripts/varga.py": "8ff96b9fc8346ca249abff6109434b6ed57fa150e1a9854d4f73bdf317568368"
|
||||
},
|
||||
"hash_scope": "explicit_identity_file_sets_not_a_transitive_dependency_lock"
|
||||
},
|
||||
"matches_metrics_scorer": false,
|
||||
"official_eval_implementation_hash_matches": false,
|
||||
"official_eval_trial_count": 0,
|
||||
"source_report": "docs/research/sealed_holdout_rerun_upstream_sync8_2026_10_18.json",
|
||||
"source_report": "docs/research/sealed_holdout_rerun_upstream_sync9b_2026_10_28.json",
|
||||
"fixed_protocol_rerun_trial_count": 20,
|
||||
"fixed_protocol_rerun_hash_matches": true,
|
||||
"metrics": {
|
||||
@@ -113,6 +113,106 @@
|
||||
}
|
||||
},
|
||||
"current_tree_fixed_protocol_rerun": {
|
||||
"evaluated_on": "2026-10-28",
|
||||
"report_path": "docs/research/sealed_holdout_rerun_upstream_sync9b_2026_10_28.json",
|
||||
"freeze_record_path": "docs/research/sealed_holdout_rerun_upstream_sync9b_2026_10_28.freeze.json",
|
||||
"implementation_sha256": "b44505aa0034e486fcfc29c8611a9559333a97c1005f92e6a5356bd855122205",
|
||||
"extended_identity": {
|
||||
"historical_artifacts_manifest_sha256": "6102a26a840be207b5858b3a4c0509274468ae9071e86d308cbbac7371d6864e",
|
||||
"production_scoring_files": [
|
||||
"scripts/active_rectification_event_engine.py",
|
||||
"scripts/active_rectification_events.py",
|
||||
"scripts/ashtakavarga.py",
|
||||
"scripts/dasha_analyzer.py",
|
||||
"scripts/divisional_charts_extended.py",
|
||||
"scripts/domain_calculation_service.py",
|
||||
"scripts/functional_benefics.py",
|
||||
"scripts/jaimini.py",
|
||||
"scripts/minute_rectification_fact_ranker_v4.py",
|
||||
"scripts/minute_rectification_feature_facts_v4.py",
|
||||
"scripts/narayana_dasha.py",
|
||||
"scripts/rectification/api_service.py",
|
||||
"scripts/rectification/candidate_contrast.py",
|
||||
"scripts/rectification/candidate_window.py",
|
||||
"scripts/rectification/case_holdout.py",
|
||||
"scripts/rectification/contracts.py",
|
||||
"scripts/rectification/dasha_transition_proximity.py",
|
||||
"scripts/rectification/decision_policy.py",
|
||||
"scripts/rectification/event_probes.py",
|
||||
"scripts/rectification/refinement_packet.py",
|
||||
"scripts/rectification/scoring_service.py",
|
||||
"scripts/shadbala.py",
|
||||
"scripts/varga.py"
|
||||
],
|
||||
"production_scoring_sha256": "7a727418651321d1b9073588587050c4dc6292587748cf35734dd76cefb0effe",
|
||||
"research_files": [
|
||||
"scripts/research/reported_offset_sweep.py",
|
||||
"scripts/research/cluster_width_lib.py",
|
||||
"scripts/research/probe_supply_after_six.py",
|
||||
"scripts/research/sealed_holdout_rerun.py",
|
||||
"scripts/minute_rectification_blind_eval.py",
|
||||
"scripts/minute_rectification_fact_blind_eval_v4.py",
|
||||
"scripts/minute_rectification_holdout_validator.py"
|
||||
],
|
||||
"research_implementation_sha256": "6fa0b0c335ac0cdc25ee887d432ae2f1f6487c665aed9665ad5cc0bc6ef0283b",
|
||||
"file_sha256": {
|
||||
"scripts/active_rectification_event_engine.py": "96c537d221a84680ddaaba74e3335162c44b5bbd9a60d10e69550a2095910d98",
|
||||
"scripts/active_rectification_events.py": "03e333ccf1103caf685dfe50fe890ab1793e945037085f50ce834a4a3b232222",
|
||||
"scripts/ashtakavarga.py": "c524da3f54b2ed9c080b78b33854854a0ca3d5d79b7e536bb1c3bc2be3e02852",
|
||||
"scripts/dasha_analyzer.py": "3949b96813217fe624899856afec9f0b787c4cb0fea3a9a0d961990dbd11943a",
|
||||
"scripts/divisional_charts_extended.py": "e9820e80f14fddc86ab5495d11455d16c2d3c9502a9a242fcf9efc41186e0d11",
|
||||
"scripts/domain_calculation_service.py": "66923a2bc69daf65c965d2973c4b9234edbec5147909eaf3609b6b377ce3c7ca",
|
||||
"scripts/functional_benefics.py": "cbaac3b8c09b97f8724b5300e9a402cbf77089af6c7db8788550d40fd35ac57b",
|
||||
"scripts/jaimini.py": "6b88658da5404da9b136686d92a04be462d61a39da6e8de4255021e2ed2d1173",
|
||||
"scripts/minute_rectification_blind_eval.py": "07aa30d8bad3278669495d5fea8be937aaf6367c8af980ff4f2fe9aa84ccc6ff",
|
||||
"scripts/minute_rectification_fact_blind_eval_v4.py": "bdf460e3467208e8b87345c5beb2128f3e22de7eeffd68f72b281b3ad9affc92",
|
||||
"scripts/minute_rectification_fact_ranker_v4.py": "f1a0d8d2482cd754d2e7f253a9606b0373519435a2bf58cfc3753e2b9ae0fb76",
|
||||
"scripts/minute_rectification_feature_facts_v4.py": "3d899a2cdd4825b8e32062443d487edcd2173a4ea55a36eb9f3abe587c0faf0f",
|
||||
"scripts/minute_rectification_holdout_validator.py": "b60b1c752e58672beaee8b36eb3b2348169726cda070ec969db0f13e7ed715af",
|
||||
"scripts/narayana_dasha.py": "bc667c30c9562fb73fb0b7b4c22a5e377f9a531795141fc629b2cd3cf396773a",
|
||||
"scripts/rectification/api_service.py": "089730097d3872a4f7b5d7dd5574bb7e9ab4d2e246d4a380800bc824d7979b95",
|
||||
"scripts/rectification/candidate_contrast.py": "eee7da71c0574a08876404b78ca111153fa95fb83ace82b9fdca141d1992e273",
|
||||
"scripts/rectification/candidate_window.py": "2fbc5058b12049b15fca375cdc125b8283e19f9f3e4bedcc291e344501891bef",
|
||||
"scripts/rectification/case_holdout.py": "fe0e699b617d9e216acdfeff87af4221c1d6f4c101bf33e1dbf14e2bcd75a8b5",
|
||||
"scripts/rectification/contracts.py": "c6dc6bd230e38f9f2126c224aae9ddbd6415304c15340f8c7cd3f06f6ef3160f",
|
||||
"scripts/rectification/dasha_transition_proximity.py": "5ce4ed27b0f06c681abc34e5fd998fafb10c153a0070aa36c99ddfa869a2cbf2",
|
||||
"scripts/rectification/decision_policy.py": "80e8de4680d19a052213bb821d2a9c94143a2962235aeb27f6ad81374d2e60d5",
|
||||
"scripts/rectification/event_probes.py": "f5ca9d54501f67a1546fcf5b01da0c1277dec2e2c6df481884e8900d14909a7c",
|
||||
"scripts/rectification/refinement_packet.py": "dbeea5aa780889785ce850f3881e8b7f67bb44594e9bc382c65b2e730884a2f4",
|
||||
"scripts/rectification/scoring_service.py": "fa056fa30d9e6ffa4b75295fc49c793e35b26ebeca64ca843d26c8abef3050f2",
|
||||
"scripts/research/cluster_width_lib.py": "1d8f2ffa7795d62069deebb2de8e16749317037d4ed3f24a2c0092676e5cfcc6",
|
||||
"scripts/research/probe_supply_after_six.py": "098a5b4398d6f8d996917d809c2fc922ad64031d3b70660f8ef7c876aba92438",
|
||||
"scripts/research/reported_offset_sweep.py": "8ecc381dfaa3c0e6bea058cfa44c606aabdb1086c137f16da82ee5a3bf5ccc91",
|
||||
"scripts/research/sealed_holdout_rerun.py": "ebcdae760f2b76c81d5d0c8e2bf347cff4c81cca9074f27ae94b6734408ccd42",
|
||||
"scripts/shadbala.py": "334dc9d8e4292f0c95d40fd4c29285aa89b9e49d4f187fb9498f4bb0ad93bdfc",
|
||||
"scripts/varga.py": "8ff96b9fc8346ca249abff6109434b6ed57fa150e1a9854d4f73bdf317568368"
|
||||
},
|
||||
"hash_scope": "explicit_identity_file_sets_not_a_transitive_dependency_lock"
|
||||
},
|
||||
"scorer_frozen_before_rerun": true,
|
||||
"source_audit_status": "corrected_known_date_errors",
|
||||
"trial_count": 20,
|
||||
"official_valid_independent_blind": false,
|
||||
"is_blind_evaluation": false,
|
||||
"truth_hidden_from_ranker": true,
|
||||
"results_previously_seen": true,
|
||||
"must_not_claim_as_release_metrics": true,
|
||||
"must_not_use_for_tuning": true,
|
||||
"verified_minute_claim_allowed": false,
|
||||
"metric_gates_passed": false,
|
||||
"events_per_case": 3,
|
||||
"boundary": "Previously exposed v3 shadow fixed-protocol rerun after upstream sync9b (ERR-110, BUG-1494 to BUG-1495). Trials and metrics match the 2026-10-18 upstream sync8 report. Versus the 2026-09-20 historical report, changed_trial_count stays 8. Not a release metric. Prior sync8 report and freeze preserved byte-for-byte.",
|
||||
"report_sha256": "ef20e751a746a4ec3b368d6860123602172a9af9195a97ec5f4d3c2cdf4823ce",
|
||||
"freeze_record_sha256": "32cad06f25c84891a24725110c9737489b8d79ed7c2d33ae959cb8eb16798dab",
|
||||
"replay_started_at_utc": "2026-10-28T08:19:06.261911+00:00",
|
||||
"replay_finished_at_utc": "2026-10-28T08:19:09.621910+00:00",
|
||||
"previous_fixed_protocol_rerun": {
|
||||
"report_path": "docs/research/sealed_holdout_rerun_upstream_sync8_2026_10_18.json",
|
||||
"freeze_record_path": "docs/research/sealed_holdout_rerun_upstream_sync8_2026_10_18.freeze.json",
|
||||
"superseded_reason": "Upstream sync9b (BUG-1494 to BUG-1495) changed scripts/varga.py, which is part of the extended identity. Output is unchanged, so ERR-110 stored a new record. Prior sync8 report and freeze preserved byte-for-byte."
|
||||
}
|
||||
},
|
||||
"previous_fixed_protocol_rerun": {
|
||||
"evaluated_on": "2026-10-18",
|
||||
"report_path": "docs/research/sealed_holdout_rerun_upstream_sync8_2026_10_18.json",
|
||||
"freeze_record_path": "docs/research/sealed_holdout_rerun_upstream_sync8_2026_10_18.freeze.json",
|
||||
@@ -212,106 +312,6 @@
|
||||
"superseded_reason": "Upstream sync8 (BUG-1380 to BUG-1390) changed frozen identity files. Output is unchanged, so ERR-110 stored a new record. Prior scoring_perf report and freeze preserved byte-for-byte."
|
||||
}
|
||||
},
|
||||
"previous_fixed_protocol_rerun": {
|
||||
"evaluated_on": "2026-10-03",
|
||||
"report_path": "docs/research/sealed_holdout_rerun_astrologer_rulings_batch1_2026_10_03.json",
|
||||
"freeze_record_path": "docs/research/sealed_holdout_rerun_astrologer_rulings_batch1_2026_10_03.freeze.json",
|
||||
"implementation_sha256": "114c60c45fa35a77bb6bfe2c177bbcc90b2e0748e2a33e61975f7393802f96f2",
|
||||
"extended_identity": {
|
||||
"historical_artifacts_manifest_sha256": "6102a26a840be207b5858b3a4c0509274468ae9071e86d308cbbac7371d6864e",
|
||||
"production_scoring_files": [
|
||||
"scripts/active_rectification_event_engine.py",
|
||||
"scripts/active_rectification_events.py",
|
||||
"scripts/ashtakavarga.py",
|
||||
"scripts/dasha_analyzer.py",
|
||||
"scripts/divisional_charts_extended.py",
|
||||
"scripts/domain_calculation_service.py",
|
||||
"scripts/functional_benefics.py",
|
||||
"scripts/jaimini.py",
|
||||
"scripts/minute_rectification_fact_ranker_v4.py",
|
||||
"scripts/minute_rectification_feature_facts_v4.py",
|
||||
"scripts/narayana_dasha.py",
|
||||
"scripts/rectification/api_service.py",
|
||||
"scripts/rectification/candidate_contrast.py",
|
||||
"scripts/rectification/candidate_window.py",
|
||||
"scripts/rectification/case_holdout.py",
|
||||
"scripts/rectification/contracts.py",
|
||||
"scripts/rectification/dasha_transition_proximity.py",
|
||||
"scripts/rectification/decision_policy.py",
|
||||
"scripts/rectification/event_probes.py",
|
||||
"scripts/rectification/refinement_packet.py",
|
||||
"scripts/rectification/scoring_service.py",
|
||||
"scripts/shadbala.py",
|
||||
"scripts/varga.py"
|
||||
],
|
||||
"production_scoring_sha256": "788745f7b7aba1d551a73a67a56daff3e730c1d408cd77dde155e410ad9d00bb",
|
||||
"research_files": [
|
||||
"scripts/research/reported_offset_sweep.py",
|
||||
"scripts/research/cluster_width_lib.py",
|
||||
"scripts/research/probe_supply_after_six.py",
|
||||
"scripts/research/sealed_holdout_rerun.py",
|
||||
"scripts/minute_rectification_blind_eval.py",
|
||||
"scripts/minute_rectification_fact_blind_eval_v4.py",
|
||||
"scripts/minute_rectification_holdout_validator.py"
|
||||
],
|
||||
"research_implementation_sha256": "fa74b52f52ca7beeb1c6c6647b11ef075c16bcb6fadb656196770d91004cd0ad",
|
||||
"file_sha256": {
|
||||
"scripts/active_rectification_event_engine.py": "cdc0e68edc770a2a1810fbdb68cbfd46dcd50b267c739c496fcc078c5e26b5b4",
|
||||
"scripts/active_rectification_events.py": "03e333ccf1103caf685dfe50fe890ab1793e945037085f50ce834a4a3b232222",
|
||||
"scripts/ashtakavarga.py": "9bfc0b8a06af6122fa6c3881706e64f28b9ba58b53d98ff55957d4741d820f3f",
|
||||
"scripts/dasha_analyzer.py": "5e53b1b4d4c7414c08dd0e7716f120a63c70ec3428ec93547f64d55c580da0e1",
|
||||
"scripts/divisional_charts_extended.py": "73bd50a83ac444830940184d0988cea621855f91db76a3f1a232014b992eacea",
|
||||
"scripts/domain_calculation_service.py": "9ee955874eea806c7b56f4246d90009c02ee8318b8f3dfe1d7132b1c0b4262e1",
|
||||
"scripts/functional_benefics.py": "cbaac3b8c09b97f8724b5300e9a402cbf77089af6c7db8788550d40fd35ac57b",
|
||||
"scripts/jaimini.py": "6b88658da5404da9b136686d92a04be462d61a39da6e8de4255021e2ed2d1173",
|
||||
"scripts/minute_rectification_blind_eval.py": "07aa30d8bad3278669495d5fea8be937aaf6367c8af980ff4f2fe9aa84ccc6ff",
|
||||
"scripts/minute_rectification_fact_blind_eval_v4.py": "bdf460e3467208e8b87345c5beb2128f3e22de7eeffd68f72b281b3ad9affc92",
|
||||
"scripts/minute_rectification_fact_ranker_v4.py": "f1a0d8d2482cd754d2e7f253a9606b0373519435a2bf58cfc3753e2b9ae0fb76",
|
||||
"scripts/minute_rectification_feature_facts_v4.py": "3d899a2cdd4825b8e32062443d487edcd2173a4ea55a36eb9f3abe587c0faf0f",
|
||||
"scripts/minute_rectification_holdout_validator.py": "b60b1c752e58672beaee8b36eb3b2348169726cda070ec969db0f13e7ed715af",
|
||||
"scripts/narayana_dasha.py": "7ff2c3238cd113b11b815e14b41967bd8e486ff62e36650e55613b9c79d1bfe3",
|
||||
"scripts/rectification/api_service.py": "e316ced0f3796d0378a0fe1aad950ee25922ae8afd53add578ed59ca8b2f0f79",
|
||||
"scripts/rectification/candidate_contrast.py": "eee7da71c0574a08876404b78ca111153fa95fb83ace82b9fdca141d1992e273",
|
||||
"scripts/rectification/candidate_window.py": "2fbc5058b12049b15fca375cdc125b8283e19f9f3e4bedcc291e344501891bef",
|
||||
"scripts/rectification/case_holdout.py": "fe0e699b617d9e216acdfeff87af4221c1d6f4c101bf33e1dbf14e2bcd75a8b5",
|
||||
"scripts/rectification/contracts.py": "c6dc6bd230e38f9f2126c224aae9ddbd6415304c15340f8c7cd3f06f6ef3160f",
|
||||
"scripts/rectification/dasha_transition_proximity.py": "0afba494aa12899621173c3cc941c6c1d0be9cec0bdf4c4a1c9bcd004ab22de8",
|
||||
"scripts/rectification/decision_policy.py": "80e8de4680d19a052213bb821d2a9c94143a2962235aeb27f6ad81374d2e60d5",
|
||||
"scripts/rectification/event_probes.py": "9bbadc246e6b8e94bb047522c3e5645613f508b3473ef40b38a207c77fdad58c",
|
||||
"scripts/rectification/refinement_packet.py": "dbeea5aa780889785ce850f3881e8b7f67bb44594e9bc382c65b2e730884a2f4",
|
||||
"scripts/rectification/scoring_service.py": "4f13c6a78d2dda55454ac20e4091fce280e3626ecfe8a9d723217ef3ebc05d02",
|
||||
"scripts/research/cluster_width_lib.py": "1d8f2ffa7795d62069deebb2de8e16749317037d4ed3f24a2c0092676e5cfcc6",
|
||||
"scripts/research/probe_supply_after_six.py": "098a5b4398d6f8d996917d809c2fc922ad64031d3b70660f8ef7c876aba92438",
|
||||
"scripts/research/reported_offset_sweep.py": "f63c1c58d334e083577736397b4acc00757550fa3b863b02784c9d6f0f018c80",
|
||||
"scripts/research/sealed_holdout_rerun.py": "1178ebfb9dd61d4aefe9961d83f19e7a943a4ca3de9fa2efffff6f8ba8c09161",
|
||||
"scripts/shadbala.py": "e949ff1cc925ccbc9a25b68981091d1d621b80a5b3839a3b3ade655d082424d6",
|
||||
"scripts/varga.py": "4331de5a25ea08729af91aae863943c4223183937f419d52dc63fd6fe7f27be6"
|
||||
},
|
||||
"hash_scope": "explicit_identity_file_sets_not_a_transitive_dependency_lock"
|
||||
},
|
||||
"scorer_frozen_before_rerun": true,
|
||||
"source_audit_status": "corrected_known_date_errors",
|
||||
"trial_count": 20,
|
||||
"official_valid_independent_blind": false,
|
||||
"is_blind_evaluation": false,
|
||||
"truth_hidden_from_ranker": true,
|
||||
"results_previously_seen": true,
|
||||
"must_not_claim_as_release_metrics": true,
|
||||
"must_not_use_for_tuning": true,
|
||||
"verified_minute_claim_allowed": false,
|
||||
"metric_gates_passed": false,
|
||||
"events_per_case": 3,
|
||||
"boundary": "Previously exposed v3 shadow fixed-protocol rerun after the 2026-10-03 astrologer rulings batch 1 re-freeze. Superseded by the 2026-10-07 upstream sync 6 re-freeze (ERR-110: scripts/varga.py, scripts/divisional_charts_extended.py, scripts/narayana_dasha.py and scripts/rectification/scoring_service.py bytes changed by the PVR 6.2 D5/D6/D8/D11 maps and the scoring-11 identity, BUG-1260/1264). Shadow trial fields versus the 2026-09-20 historical report are unchanged (changed_trial_count 0). Report and freeze preserved byte-for-byte.",
|
||||
"report_sha256": "b39b53c1f2aa0ad6ac363b4c359116c30bc89b446cb670e6172c8cee5ccc5efd",
|
||||
"freeze_record_sha256": "c1c11e7abc03b9174aeb1e0b280f550808f887818f578e5d10ad667e0da67e24",
|
||||
"replay_started_at_utc": "2026-10-03T06:53:04.851491+00:00",
|
||||
"replay_finished_at_utc": "2026-10-03T06:53:07.509845+00:00",
|
||||
"previous_fixed_protocol_rerun": {
|
||||
"report_path": "docs/research/sealed_holdout_rerun_upstream_sync5_2026_10_02.json",
|
||||
"freeze_record_path": "docs/research/sealed_holdout_rerun_upstream_sync5_2026_10_02.freeze.json",
|
||||
"superseded_reason": "Upstream sync round 6 (ERR-110). Prior sync5 report and freeze preserved byte-for-byte."
|
||||
}
|
||||
},
|
||||
"current_tree_unfrozen_diagnostic": {
|
||||
"is_blind_evaluation": false,
|
||||
"results_already_seen": true,
|
||||
@@ -330,6 +330,28 @@
|
||||
"verified_minute_claim_allowed": false
|
||||
},
|
||||
"current_tree_reported_offset_replay": {
|
||||
"report_path": "docs/research/reported_offset_upstream_sync9b_2026_10_28.json",
|
||||
"report_sha256": "880bebc2e217100994a1442e47ead3f9d33e48aa3ac20f69d993f38def3ef25c",
|
||||
"freeze_record_path": "docs/research/reported_offset_upstream_sync9b_2026_10_28.freeze.json",
|
||||
"freeze_record_sha256": "385817bb27f04afeba230af5e547dd10dc8cf6db4b25c18bebe4f4e674cab972",
|
||||
"scope": "reported_offset_sensitivity_not_product_accuracy",
|
||||
"trial_count": 900,
|
||||
"official_valid_independent_blind": false,
|
||||
"official_blind_trial_count": 0,
|
||||
"must_not_use_for_tuning": true,
|
||||
"must_not_claim_as_release_metrics": true,
|
||||
"replay_started_at_utc": "2026-10-28T08:19:10.206540+00:00",
|
||||
"replay_finished_at_utc": "2026-10-28T08:23:34.348635+00:00",
|
||||
"boundary": "Previously exposed reported-offset sensitivity after upstream sync9b (ERR-110, BUG-1494 to BUG-1495). Summary and trials match the 2026-10-18 upstream sync8 report. Versus the 2026-09-20 historical report, changed_trial_count stays 581 of 900. Not a release metric. No tuning. Prior sync8 report and freeze preserved byte-for-byte.",
|
||||
"previous_reported_offset_replay": {
|
||||
"report_path": "docs/research/reported_offset_upstream_sync8_2026_10_18.json",
|
||||
"report_sha256": "a51aad2e743dccba7726d1a0a5e44c5e431ff429fe1725c9be2f7fa5e9282da6",
|
||||
"freeze_record_path": "docs/research/reported_offset_upstream_sync8_2026_10_18.freeze.json",
|
||||
"freeze_record_sha256": "117ab0bfa4a283e9d52073808f71dd8317f613c96b27ec271d887ef40087f4f5",
|
||||
"superseded_reason": "Upstream sync9b (BUG-1494 to BUG-1495) changed scripts/varga.py, which is part of the extended identity. Output is unchanged, so ERR-110 stored a new record. Prior sync8 report and freeze preserved byte-for-byte."
|
||||
}
|
||||
},
|
||||
"previous_reported_offset_replay": {
|
||||
"report_path": "docs/research/reported_offset_upstream_sync8_2026_10_18.json",
|
||||
"report_sha256": "a51aad2e743dccba7726d1a0a5e44c5e431ff429fe1725c9be2f7fa5e9282da6",
|
||||
"freeze_record_path": "docs/research/reported_offset_upstream_sync8_2026_10_18.freeze.json",
|
||||
@@ -350,12 +372,5 @@
|
||||
"freeze_record_sha256": "1560f24ffea19116c904272907ff9cce24904960406676f3aada3c8c174cf98a",
|
||||
"superseded_reason": "Upstream sync8 (BUG-1380 to BUG-1390) changed frozen identity files. Output is unchanged, so ERR-110 stored a new record. Versus the 2026-09-20 historical report, changed_trial_count stays 581 of 900. Prior scoring_perf report and freeze preserved byte-for-byte."
|
||||
}
|
||||
},
|
||||
"previous_reported_offset_replay": {
|
||||
"report_path": "docs/research/reported_offset_astrologer_rulings_batch1_2026_10_03.json",
|
||||
"report_sha256": "480eb771c2c7e5a300589561dc66be950a711ffa205a8fa370db6a11f1e04270",
|
||||
"freeze_record_path": "docs/research/reported_offset_astrologer_rulings_batch1_2026_10_03.freeze.json",
|
||||
"freeze_record_sha256": "6be6a997f610c341aecc1f782fa27a5b86b66d43d00fcfa10750c81189c34748",
|
||||
"superseded_reason": "Upstream sync 6 (ERR-110: scripts/varga.py, scripts/divisional_charts_extended.py, scripts/narayana_dasha.py and scripts/rectification/scoring_service.py). 5 of 900 trials differ from the batch1 replay (delivery_width_minutes on 4, top_1_minute_error on 1). Versus the 2026-09-20 historical report, changed_trial_count is 235 (batch1 was 230). Prior report and freeze preserved byte-for-byte."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,11 +30,11 @@ from scripts.research.sealed_holdout_rerun import (
|
||||
OFFSETS = (-30, -20, -15, -10, -8, -5, -3, 0, 3, 5, 8, 10, 15, 20, 30)
|
||||
RADII = (15, 30, 60)
|
||||
MINUTE_STEP = 1
|
||||
# 原值: docs/research/reported_offset_scoring_perf_2026_10_13.{freeze.json,json}
|
||||
# 新值: docs/research/reported_offset_upstream_sync8_2026_10_18.{freeze.json,json}
|
||||
# 原因: sync8(BUG-1380~1389)改了 ashtakavarga / dasha_analyzer / narayana_dasha,输出不变,ERR-110 另存新记录
|
||||
FREEZE = ROOT / "docs/research/reported_offset_upstream_sync8_2026_10_18.freeze.json"
|
||||
REPORT = ROOT / "docs/research/reported_offset_upstream_sync8_2026_10_18.json"
|
||||
# 原值: docs/research/reported_offset_upstream_sync8_2026_10_18.{freeze.json,json}
|
||||
# 新值: docs/research/reported_offset_upstream_sync9b_2026_10_28.{freeze.json,json}
|
||||
# 原因: sync9b(BUG-1494~1495)改了 scripts/varga.py,它在扩展身份里;报时窗口输出不变,ERR-110 另存新记录
|
||||
FREEZE = ROOT / "docs/research/reported_offset_upstream_sync9b_2026_10_28.freeze.json"
|
||||
REPORT = ROOT / "docs/research/reported_offset_upstream_sync9b_2026_10_28.json"
|
||||
LEGACY_REPORT = ROOT / "docs/research/reported_offset_2026_09_20.json"
|
||||
|
||||
|
||||
|
||||
@@ -27,11 +27,11 @@ from scripts.minute_rectification_feature_facts_v4 import build_feature_fact_row
|
||||
from scripts.minute_rectification_holdout_validator import validate
|
||||
|
||||
DATASET = ROOT / "references/real_case_calibration/minute_rectification_holdout_v3.json"
|
||||
# 原值: docs/research/sealed_holdout_rerun_scoring_perf_2026_10_13.{freeze.json,json}
|
||||
# 新值: docs/research/sealed_holdout_rerun_upstream_sync8_2026_10_18.{freeze.json,json}
|
||||
# 原因: sync8(BUG-1380~1389)改了 ashtakavarga / dasha_analyzer / narayana_dasha,输出不变,ERR-110 另存新记录
|
||||
FREEZE = ROOT / "docs/research/sealed_holdout_rerun_upstream_sync8_2026_10_18.freeze.json"
|
||||
REPORT = ROOT / "docs/research/sealed_holdout_rerun_upstream_sync8_2026_10_18.json"
|
||||
# 原值: docs/research/sealed_holdout_rerun_upstream_sync8_2026_10_18.{freeze.json,json}
|
||||
# 新值: docs/research/sealed_holdout_rerun_upstream_sync9b_2026_10_28.{freeze.json,json}
|
||||
# 原因: sync9b(BUG-1494~1495)改了 scripts/varga.py,它在扩展身份里;切盘输出不变,ERR-110 另存新记录
|
||||
FREEZE = ROOT / "docs/research/sealed_holdout_rerun_upstream_sync9b_2026_10_28.freeze.json"
|
||||
REPORT = ROOT / "docs/research/sealed_holdout_rerun_upstream_sync9b_2026_10_28.json"
|
||||
LEGACY_REPORT = ROOT / "docs/research/sealed_holdout_rerun_2026_09_20.json"
|
||||
ARCHIVE = ROOT / "docs/research/history/rectification_pre_cross_midnight_2026_09_20"
|
||||
PRODUCTION_FILES = [
|
||||
|
||||
+12
-8
@@ -246,11 +246,12 @@ def calc_64th_navamsa(moon_lon: float) -> Dict:
|
||||
def calc_22nd_drekkana(lagna_lon: float) -> Dict:
|
||||
"""Compute the 22nd Drekkana from the Lagna's Drekkana position.
|
||||
|
||||
Traditional counting is inclusive, so the 22nd point is +21 signs from the
|
||||
Lagna Drekkana anchor.
|
||||
Crux of Vedic Astrology, printed p.329 (corpus page 340), identifies
|
||||
this as the eighth house in D3. Counting 21 ten-degree divisions is
|
||||
not the same as moving 21 signs in the D3 chart.
|
||||
"""
|
||||
base = calc_varga(lagna_lon, 3)
|
||||
sign_idx = (base['sign_idx'] + 21) % 12
|
||||
sign_idx = (base['sign_idx'] + 7) % 12
|
||||
return {
|
||||
'base_drekkana_sign_idx': base['sign_idx'],
|
||||
'base_drekkana_sign': base['sign'],
|
||||
@@ -279,16 +280,19 @@ def calc_bhrigu_bindu(moon_lon: float, rahu_lon: float) -> Dict:
|
||||
}
|
||||
|
||||
|
||||
SARPA_DREKKANA_SIGNS = {'Cancer': 2, 'Scorpio': 1, 'Pisces': 3}
|
||||
# Raman, Hindu Predictive Astrology, Drekkanas chapter, corpus pp.279–283:
|
||||
# five serpent divisions, including both Cancer-3 and Scorpio-2.
|
||||
SARPA_DREKKANA_SIGNS = {'Cancer': (2, 3), 'Scorpio': (1, 2), 'Pisces': (3,)}
|
||||
|
||||
|
||||
def calc_sarpa_drekkana(lon: float) -> Dict:
|
||||
"""Classify Sarpa Drekkana using the classical water-sign sensitive drekkanas."""
|
||||
lon = lon % 360
|
||||
base = calc_varga(lon, 3)
|
||||
sign = _sn(_si(lon))
|
||||
degree_in_sign = round((lon % 30), 4)
|
||||
drekkana_number = int(degree_in_sign / 10) + 1
|
||||
expected = SARPA_DREKKANA_SIGNS.get(sign)
|
||||
drekkana_number = int((lon % 30) / 10) + 1
|
||||
expected = SARPA_DREKKANA_SIGNS.get(sign, ())
|
||||
return {
|
||||
'longitude': round(lon % 360, 4),
|
||||
'sign': sign,
|
||||
@@ -297,8 +301,8 @@ def calc_sarpa_drekkana(lon: float) -> Dict:
|
||||
'drekkana_number': drekkana_number,
|
||||
'd3_sign': base['sign'],
|
||||
'd3_sign_idx': base['sign_idx'],
|
||||
'is_sarpa_drekkana': bool(expected == drekkana_number),
|
||||
'definition': 'Cancer-2, Scorpio-1, Pisces-3',
|
||||
'is_sarpa_drekkana': drekkana_number in expected,
|
||||
'definition': 'Cancer-2, Cancer-3, Scorpio-1, Scorpio-2, Pisces-3',
|
||||
}
|
||||
|
||||
def dignity(planet, sign_idx):
|
||||
|
||||
@@ -701,7 +701,7 @@ def test_full_reading_exposes_sensitive_point_modules() -> None:
|
||||
assert sensitive["bhrigu_bindu"]["sign"]
|
||||
assert isinstance(sensitive["sarpa_drekkana"], dict)
|
||||
for payload in sensitive["sarpa_drekkana"].values():
|
||||
assert payload["definition"] == "Cancer-2, Scorpio-1, Pisces-3"
|
||||
assert payload["definition"] == "Cancer-2, Cancer-3, Scorpio-1, Scorpio-2, Pisces-3"
|
||||
assert payload["is_sarpa_drekkana"] is True
|
||||
|
||||
|
||||
|
||||
@@ -53,6 +53,11 @@ IDENTITY_FORBIDDEN_PREFIXES = (
|
||||
# 原因: sync8 改了冻结身份文件,输出不变,ERR-110 另存新记录
|
||||
"docs/research/sealed_holdout_rerun_upstream_sync8_2026_10_18",
|
||||
"docs/research/reported_offset_upstream_sync8_2026_10_18",
|
||||
# 原值: 排除到 upstream_sync8_2026_10_18
|
||||
# 新值: 再排除 upstream_sync9b_2026_10_28 两份冻结路径(追加,不替换上面的)
|
||||
# 原因: sync9b 改了扩展身份里的 scripts/varga.py,回放输出不变,ERR-110 另存新记录
|
||||
"docs/research/sealed_holdout_rerun_upstream_sync9b_2026_10_28",
|
||||
"docs/research/reported_offset_upstream_sync9b_2026_10_28",
|
||||
"docs/research/history/",
|
||||
"artifacts/",
|
||||
)
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
"""Classification must use raw longitude, never rounded display degrees."""
|
||||
import pytest
|
||||
|
||||
from scripts.varga import calc_sarpa_drekkana
|
||||
|
||||
|
||||
@pytest.mark.parametrize('longitude,part,is_sarpa', [
|
||||
(99.99999, 1, False), (100.0, 2, True),
|
||||
(219.99999, 1, True), (220.0, 2, True),
|
||||
(359.99999, 3, True), (360.0, 1, False),
|
||||
])
|
||||
def test_sarpa_partition_uses_unrounded_longitude(longitude, part, is_sarpa):
|
||||
row = calc_sarpa_drekkana(longitude)
|
||||
assert row['drekkana_number'] == part
|
||||
assert row['is_sarpa_drekkana'] is is_sarpa
|
||||
assert 1 <= row['drekkana_number'] <= 3
|
||||
@@ -0,0 +1,225 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Regression tests for BPHS divisional chart mappings."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
pytest.importorskip(
|
||||
"hypothesis",
|
||||
reason="hypothesis is not installed; see requirements-dev.txt. "
|
||||
"The guard keeps this file collectable: without it the whole file "
|
||||
"fails to collect and its tests vanish from the run instead of failing.",
|
||||
)
|
||||
|
||||
|
||||
from hypothesis import given
|
||||
from hypothesis import strategies as st
|
||||
from varga import (
|
||||
SIGNS,
|
||||
calc_22nd_drekkana,
|
||||
calc_64th_navamsa,
|
||||
calc_bhrigu_bindu,
|
||||
calc_all_vargas,
|
||||
calc_sarpa_drekkana,
|
||||
calc_varga,
|
||||
varga_map,
|
||||
)
|
||||
|
||||
|
||||
def navamsa_ref(lon: float) -> int:
|
||||
sign_index = int((lon % 360) / 30) % 12
|
||||
degree_in_sign = (lon % 360) - sign_index * 30
|
||||
part_index = int(degree_in_sign / (30 / 9))
|
||||
if sign_index % 3 == 0:
|
||||
start = sign_index
|
||||
elif sign_index % 3 == 1:
|
||||
start = (sign_index + 8) % 12
|
||||
else:
|
||||
start = (sign_index + 4) % 12
|
||||
return (start + part_index) % 12
|
||||
|
||||
|
||||
def dasamsa_ref(lon: float) -> int:
|
||||
sign_index = int((lon % 360) / 30) % 12
|
||||
degree_in_sign = (lon % 360) - sign_index * 30
|
||||
part_index = int(degree_in_sign / 3)
|
||||
start = sign_index if sign_index % 2 == 0 else (sign_index + 8) % 12
|
||||
return (start + part_index) % 12
|
||||
|
||||
|
||||
def drekkana_ref(lon: float) -> int:
|
||||
sign_index = int((lon % 360) / 30) % 12
|
||||
degree_in_sign = (lon % 360) - sign_index * 30
|
||||
part_index = int(degree_in_sign / 10)
|
||||
return (sign_index + part_index * 4) % 12
|
||||
|
||||
|
||||
def chaturthamsa_ref(lon: float) -> int:
|
||||
sign_index = int((lon % 360) / 30) % 12
|
||||
degree_in_sign = (lon % 360) - sign_index * 30
|
||||
part_index = int(degree_in_sign / (30 / 4))
|
||||
return (sign_index + part_index * 3) % 12
|
||||
|
||||
|
||||
@given(st.floats(min_value=0, max_value=359.999999, allow_nan=False, allow_infinity=False))
|
||||
def test_navamsa_matches_bphs_reference(lon: float) -> None:
|
||||
result = calc_varga(lon, 9)
|
||||
assert result["sign_idx"] == navamsa_ref(lon)
|
||||
assert result["sign"] == SIGNS[navamsa_ref(lon)]
|
||||
assert 0 <= result["degree_in_sign"] < 30
|
||||
|
||||
|
||||
@given(st.floats(min_value=0, max_value=359.999999, allow_nan=False, allow_infinity=False))
|
||||
def test_dasamsa_matches_bphs_reference(lon: float) -> None:
|
||||
result = calc_varga(lon, 10)
|
||||
assert result["sign_idx"] == dasamsa_ref(lon)
|
||||
assert result["sign"] == SIGNS[dasamsa_ref(lon)]
|
||||
assert 0 <= result["degree_in_sign"] < 30
|
||||
|
||||
|
||||
@given(st.floats(min_value=0, max_value=359.999999, allow_nan=False, allow_infinity=False))
|
||||
def test_drekkana_uses_same_plus_four_plus_eight(lon: float) -> None:
|
||||
result = calc_varga(lon, 3)
|
||||
assert result["sign_idx"] == drekkana_ref(lon)
|
||||
|
||||
|
||||
@given(st.floats(min_value=0, max_value=359.999999, allow_nan=False, allow_infinity=False))
|
||||
def test_chaturthamsa_matches_pyjhora_parashara_reference(lon: float) -> None:
|
||||
result = calc_varga(lon, 4)
|
||||
assert result["sign_idx"] == chaturthamsa_ref(lon)
|
||||
assert result["sign"] == SIGNS[chaturthamsa_ref(lon)]
|
||||
assert 0 <= result["degree_in_sign"] < 30
|
||||
|
||||
|
||||
def test_chaturthamsa_reference_boundary_examples() -> None:
|
||||
assert [varga_map(0, part, 4) for part in range(4)] == [0, 3, 6, 9]
|
||||
assert [varga_map(8, part, 4) for part in range(4)] == [8, 11, 2, 5]
|
||||
assert [varga_map(11, part, 4) for part in range(4)] == [11, 2, 5, 8]
|
||||
|
||||
|
||||
def test_varga_map_boundary_examples() -> None:
|
||||
assert varga_map(0, 0, 9) == 0 # Aries Navamsa starts Aries
|
||||
assert varga_map(1, 0, 9) == 9 # Taurus Navamsa starts Capricorn (9th from sign)
|
||||
assert varga_map(2, 0, 9) == 6 # Gemini Navamsa starts Libra (5th from sign)
|
||||
assert varga_map(1, 0, 10) == 9 # Taurus Dasamsa starts Capricorn
|
||||
assert varga_map(0, 2, 3) == 8 # Aries third Drekkana = Sagittarius
|
||||
|
||||
|
||||
def test_rudramsa_d11_is_supported_by_native_varga_map() -> None:
|
||||
assert varga_map(0, 0, 11) == 0
|
||||
assert varga_map(0, 10, 11) == 10
|
||||
# PVR 6.2.11: anti-zodiacal seed from Aries, zodiacal parts.
|
||||
assert varga_map(1, 0, 11) == 11
|
||||
assert varga_map(1, 10, 11) == 9
|
||||
result = calc_varga(35.0, 11)
|
||||
assert result["sign"] == "Aries"
|
||||
assert 0 <= result["degree_in_sign"] < 30
|
||||
|
||||
|
||||
def test_native_varga_map_supports_declared_high_value_vargas() -> None:
|
||||
for div in (2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 24, 60, 81, 108, 144):
|
||||
result = calc_varga(35.0, div)
|
||||
assert result["sign"] in SIGNS
|
||||
assert 0 <= result["degree_in_sign"] < 30
|
||||
|
||||
|
||||
def test_default_varga_batch_includes_declared_vargas() -> None:
|
||||
charts = calc_all_vargas({"Moon": 35.0}, 10.0)
|
||||
for key in ("D5_Panchamsa", "D6_Shashthamsa", "D8_Ashtamsa", "D11_Rudramsa", "D81_Navamsa-Navamsa", "D108_Dwadasamsa-Navamsa", "D144_Dwadasamsa-Dwadasamsa"):
|
||||
assert key in charts
|
||||
assert charts["D11_Rudramsa"]["Moon"]["sign"] == "Aries"
|
||||
|
||||
|
||||
def test_navamsa_matches_user_jhora_pdf_reference_chart() -> None:
|
||||
"""Regression from private_chart_reference.pdf: JHora-style D9 table for public sample birth datetime San Francisco."""
|
||||
expected = {
|
||||
132.355025: "Cancer", # Ascendant 12 Leo 21'18.09"
|
||||
3.5226611111111112: "Taurus",
|
||||
311.78995555555554: "Capricorn",
|
||||
91.33091388888889: "Cancer",
|
||||
338.5488: "Virgo",
|
||||
163.83150833333335: "Taurus",
|
||||
340.5554638888889: "Libra",
|
||||
304.3033805555556: "Scorpio",
|
||||
231.04509444444443: "Capricorn",
|
||||
51.045094444444445: "Cancer",
|
||||
}
|
||||
for longitude, expected_sign in expected.items():
|
||||
assert calc_varga(longitude, 9)["sign"] == expected_sign
|
||||
|
||||
|
||||
def test_64th_navamsa_counts_forward_from_moon_navamsa() -> None:
|
||||
moon_lon = 42.3
|
||||
result = calc_64th_navamsa(moon_lon)
|
||||
expected_sign_idx = (navamsa_ref(moon_lon) + 63) % 12
|
||||
assert result["sign_idx"] == expected_sign_idx
|
||||
assert result["sign"] == SIGNS[expected_sign_idx]
|
||||
assert result["offset_from_moon_navamsa"] == 64
|
||||
|
||||
|
||||
def test_22nd_drekkana_counts_forward_from_lagna_drekkana() -> None:
|
||||
asc_lon = 25.5
|
||||
result = calc_22nd_drekkana(asc_lon)
|
||||
# Sagittarius D3 rising: its eighth house is Cancer (Crux p.329).
|
||||
expected_sign_idx = 3
|
||||
assert result["sign_idx"] == expected_sign_idx
|
||||
assert result["sign"] == SIGNS[expected_sign_idx]
|
||||
assert result["offset_from_lagna_drekkana"] == 22
|
||||
|
||||
|
||||
def test_22nd_drekkana_independent_zodiac_partition_count() -> None:
|
||||
# Enumerate all 36 zodiacal ten-degree divisions, then count 22 inclusively.
|
||||
# This oracle does not add an offset to the calculated D3 sign.
|
||||
for division in range(36):
|
||||
target = (division + 21) % 36
|
||||
target_rasi, target_part = divmod(target, 3)
|
||||
expected = (target_rasi + 4 * target_part) % 12
|
||||
for within in (0.0, 0.001, 5.0, 9.999):
|
||||
result = calc_22nd_drekkana(division * 10 + within)
|
||||
assert result['sign_idx'] == expected
|
||||
|
||||
|
||||
def test_bhrigu_bindu_uses_rahu_to_moon_arc_midpoint() -> None:
|
||||
result = calc_bhrigu_bindu(270.5, 160.2333333333)
|
||||
assert result["longitude"] == 215.3667
|
||||
assert result["sign"] == "Scorpio"
|
||||
assert result["degree_in_sign"] == 5.3667
|
||||
|
||||
|
||||
def test_bhrigu_bindu_handles_wraparound_across_zero_aries() -> None:
|
||||
result = calc_bhrigu_bindu(10.0, 350.0)
|
||||
assert result["longitude"] == 0.0
|
||||
assert result["sign"] == "Aries"
|
||||
assert result["degree_in_sign"] == 0.0
|
||||
|
||||
|
||||
def test_sarpa_drekkana_detects_classical_sensitive_ranges() -> None:
|
||||
cancer_second = calc_sarpa_drekkana(105.0)
|
||||
scorpio_first = calc_sarpa_drekkana(215.0)
|
||||
pisces_third = calc_sarpa_drekkana(355.0)
|
||||
safe_aries = calc_sarpa_drekkana(15.0)
|
||||
|
||||
assert cancer_second["is_sarpa_drekkana"] is True
|
||||
assert cancer_second["sign"] == "Cancer"
|
||||
assert cancer_second["drekkana_number"] == 2
|
||||
|
||||
assert scorpio_first["is_sarpa_drekkana"] is True
|
||||
assert scorpio_first["sign"] == "Scorpio"
|
||||
assert scorpio_first["drekkana_number"] == 1
|
||||
|
||||
assert pisces_third["is_sarpa_drekkana"] is True
|
||||
assert pisces_third["sign"] == "Pisces"
|
||||
assert pisces_third["drekkana_number"] == 3
|
||||
|
||||
assert safe_aries["is_sarpa_drekkana"] is False
|
||||
|
||||
|
||||
def test_sarpa_contains_all_five_book_ranges_and_no_others() -> None:
|
||||
# Zodiac degree intervals transcribed from Raman, not the production map.
|
||||
intervals = ((100, 120), (210, 230), (350, 360))
|
||||
for segment in range(36):
|
||||
for offset in (0.0, 0.001, 5.0, 9.999):
|
||||
longitude = segment * 10 + offset
|
||||
expected = any(start <= longitude < end for start, end in intervals)
|
||||
assert calc_sarpa_drekkana(longitude)['is_sarpa_drekkana'] == expected
|
||||
@@ -175,7 +175,8 @@ def test_64th_navamsa_counts_forward_from_moon_navamsa() -> None:
|
||||
def test_22nd_drekkana_counts_forward_from_lagna_drekkana() -> None:
|
||||
asc_lon = 25.5
|
||||
result = calc_22nd_drekkana(asc_lon)
|
||||
expected_sign_idx = (drekkana_ref(asc_lon) + 21) % 12
|
||||
# Sagittarius D3 rising: its eighth house is Cancer (Crux p.329).
|
||||
expected_sign_idx = 3
|
||||
assert result["sign_idx"] == expected_sign_idx
|
||||
assert result["sign"] == SIGNS[expected_sign_idx]
|
||||
assert result["offset_from_lagna_drekkana"] == 22
|
||||
|
||||
Reference in New Issue
Block a user