Compare commits

..
5 Commits
Author SHA1 Message Date
jesse-ux 44a93d0539 Record #237 checks against updated staging
Independent Staging Quality Gate / publish (pull_request) Blocked by required conditions
Independent Staging Quality Gate / validate-python (pull_request) In progress
Independent Staging Quality Gate / validate-frontend (pull_request) In progress
2026-11-13 10:29:02 +08:00
jesse-ux 5041521322 Merge remote-tracking branch 'origin/staging' into codex/reading-window-edge-20261113 2026-11-13 10:27:50 +08:00
jesse-ux cfa8e0ff58 Exclude report birth-window endpoints from varga crossings (#237) 2026-11-13 10:27:50 +08:00
jesse-ux 1f5367d09b Merge remote-tracking branch 'origin/codex/reading-window-edge-20261113' into codex/reading-window-edge-20261113 2026-11-13 10:04:44 +08:00
ClaudeandClaude Opus 5.5 c2f295fd82 docs(tasks): brief for #237 reading-window-edge
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DbBXozsiu43ob7B8UZCmDM
2026-11-13 09:49:44 +08:00
12 changed files with 167 additions and 13 deletions
+5
View File
@@ -1,5 +1,10 @@
# BLOCKED
## #237 · Windows 全量门禁与预检
- 快速门的三项 PyJHora 检查在同机基线复现。咨询 VedAstro 对照在门禁内波动,分支与基线单独重跑均通过。目录预检超时,定向预检复现既有历史镜像路径断言失败。
- 替代证据:报告与选盘定向回归、真实 PostgreSQL 报告档案测试和仓库隐私检查通过。详见 [#237 进度](docs/tasks/PROGRESS-reading-window-edge-20261113.md)。
## BUG-1865 · 选盘保存的计算时刻可能落在所选区间外(#227,2026-11-13)
- ~~旧流程区间缺少采纳凭据。~~ 修订任务书已明确优先读取档案的选盘采纳记录,并限定旧流程的凭据。
+4
View File
@@ -1,5 +1,9 @@
# 印度占星 Skill 更新日志
## 2026-11-13 — 个人报告按区间内部的换座核对证据
- 生时纠正选定的区间、填写范围及默认范围,只在区间内部发生换座时标记分盘敏感。报告摘要和证据等级按同一规则判定。沿用当前 Skill 版本(BUG-1871)。
## 2026-11-13 — 个人报告显示章节拦截原因
- 页面和 Markdown 导出会解释对应章节为何未通过核对。新解读保存生成快照和逐节转换记录,便于复查和回放。
+13
View File
@@ -25229,3 +25229,16 @@
- 防复发:测试分别锁定档案权限、请求转交、缓存隔离与公开盘的候选瑜伽结果。
- 关联记录:#228、#189、BUG-1063。
- 修复版本:`codex/report-gender-20261113`,待发布。
## BUG-1871 | 个人报告把区间端点当成区间内换座
- 状态:fixed-pending-verify(待独立验收)。
- 首次发现 / 最近更新:2026-11-13 / 2026-11-13。
- 影响面:个人报告的分盘换座摘要和证据等级上限。
- 现象:已采纳侧起点的换座线被报为区间内换座,依赖相应分盘的结论降级。
- 触发条件:距最近换座的时间恰等于出生时间区间一侧的宽度。
- 根因:BUG-1864 的区间检查用包含端点的比较,既有测试也锁定了该行为。BUG-1865 让已采纳区间到达报告后,选择过的换座线经常成为端点,摘要与证据等级两处都将其判入区间。
- 修复:两处共用区间判定,未确认分钟时只计严格落在端点容差以内的内部换座。确认分钟仍按原有前后各一分钟的规则。
- 验证及防复发:合成事实覆盖只在端点换座、区间内有后续换座和两侧容差,分别检查分盘状态、摘要及各领域上限。确认分钟的端点和端点外也有回归覆盖。
- 关联记录:#237、BUG-1864、BUG-1865、BUG-1858。详见 [执行进度](tasks/PROGRESS-reading-window-edge-20261113.md)。
- 修复版本:`codex/reading-window-edge-20261113`,待发布。
+4
View File
@@ -2,6 +2,10 @@
Purpose: read this file before substantial project work. It exists to stop repeat mistakes caused by multiple Codex windows, WorkBuddy mirrors, local drafts, backup folders, and partial cloud-git visibility.
## 2026-11-13 · #237 Windows 报告预检
- 远端可见性、Python 和适配器检查通过。首次目录扫描及定向测试超时。外层命令时限延长至 180 秒后,目录扫描仍在 90 秒超时,定向测试仅余既有 `.workbuddy` 镜像路径断言失败。见 [#237 进度](../tasks/PROGRESS-reading-window-edge-20261113.md)。
## 2026-11-13 · #220 报告解读快照
- Windows 预检可见 Gitea 远端,碎片扫描与适配器步骤通过。既有 `test_preflight_fragment_scan_reports_authority_layers_and_risk_buckets` 仍要求本机不存在的 `.workbuddy` 历史镜像路径。预检退出 1。
@@ -0,0 +1,23 @@
# #237 执行进度 · 分盘换座的区间端点
Codex 基于 `origin/staging` 的 `151159927` 实施。任务书提交 `c2f295fd8` 相对规划基线 `3e6077f75` 只新增任务书。交付版本中的任务书移除原案例的具体时刻,保留验收用的相对分钟数。
报告摘要和未确认分钟的证据等级共用 `crosses_birth_window`。采纳区间、填写范围和默认范围的两侧都使用 `换座距离 < 该侧宽度 − 0.01 分钟`。0.01 分钟约为 0.6 秒,可吸收小于一秒的取整差异。合成回归分别检查离端点 0.005 分钟和 0.02 分钟的位置,后者仍判为区间内部。已确认分钟仍按包含端点的前后各一分钟规则判断。
合成事实包中,D2、D6、D8 只有起点换座,状态为 `clear_at_tolerance`,摘要写「区间内不换座」。D10、D9 在区间内仍有换座,状态为 `boundary_sensitive`。财富收入、财富留存和健康压力的上限为 C,事业和婚恋结构仍为 D。
| 原值 | 新值 | 原因 |
| --- | --- | --- |
| 填写区间后端点的财富收入上限 `D` | `C` | 后端点的换座不属于区间内部。 |
| 摘要对前端点写「区间内会换座」 | 「区间内不换座」 | 前侧换座距离等于前侧宽度。 |
| 摘要对后端点写「区间内会换座」 | 「区间内不换座」 | 后侧换座距离等于后侧宽度,前侧在搜索上限内未换座。 |
| 默认区间端点的事业上限 `D` | `C` | 默认窗口同样排除端点,用途稳定性仍为 `not_evaluated`。 |
## 验证
- 新增两项回归均在修复前复现端点误判。
- Python 定向 53 项通过,覆盖任务书的四个文件和既有选盘测试。报告出生时间范围与 worker 的前端定向 34 项通过,真实 PostgreSQL 的报告档案测试 1 项通过。Node 两组测试均无失败和取消。
- Python 快速门退出 1,1,462 项通过、2 项跳过、29 项子测试通过、4 项失败。其中 3 项 PyJHora 检查在同机基线逐项复现。另一项是既有咨询 VedAstro 对照波动,分支和基线各单独重跑一次均通过。
- Windows 预检确认 Python、Gitea 远端和适配器可用。目录扫描超时。延长定向测试时限后,只有既有 `.workbuddy` 镜像路径断言失败。详见 [预检台账](../research/pre_work_error_ledger.md)。
- 仓库隐私检查 62 项通过。
- 同步含 #210 的 staging `e5d6c99da` 后,年运审计、分盘范围、证据等级、用途稳定性和隐私合跑 89 项通过。
+1
View File
@@ -33,6 +33,7 @@
| 任务书 | 进度 | 主题 | 状态 | 落点 |
| --- | --- | --- | --- | --- |
| [#237 任务书](TASK-reading-window-edge-20261113.md) | [执行进度](PROGRESS-reading-window-edge-20261113.md) | 个人报告区间端点换座判定(BUG-1871) | 待验收 | `codex/reading-window-edge-20261113` |
| [#220 修订任务书](FIX-reading-snapshot-binding-1.md) | [执行进度](PROGRESS-reading-snapshot-binding-20261112.md) | 个人报告生成快照、逐节拦截原因和离线验收包(BUG-1863) | 待验收 | `codex/reading-snapshot-binding-20261112` |
截至 2026-09-03,`main` 停在 2026-08-16 的 `7b620c7a`,下表**没有任何一项已上线**。
@@ -0,0 +1,55 @@
# TASK:生时纠正选定一侧的起点就是已解决的换盘线,不能再算成「区间内会换座」(2026-11-13)
- Gitea:#237
- 分支:`codex/reading-window-edge-20261113`,基于 staging `3e6077f752b606fee69a9eb15853fe328801901b`
- Bug 编号:BUG-1871
- 验收:Claude(jyotisha-e6)
不得自行写「产品决定/裁定」;任务书没写的取舍一律选需拍板。不得修改或删除既有断言来让测试通过;确需修改的,写明原值、新值、原因。
## 现象(产品负责人 2026-11-13 实测报告)
选盘页采纳换盘线后一侧后,#227 部署的报告正确识别了生时纠正选定的区间。下表用相对计算时刻的合成测试输入复现误判,窗口前侧 0.3 分钟、后侧 15 分钟。事业、财富、健康三节都降成 D:
| 分盘 | 提前多少分钟换座 | 推后多少分钟换座 | 实际情况 |
|---|---|---|---|
| 财富分盘 D2 | 0.3 | 147.167 | 线后一侧整段都不换座 |
| 健康分盘 D6 | 0.3 | 24.367 | 线后一侧整段都不换座 |
| D8 | 0.3 | 18.2 | 线后一侧整段都不换座 |
| 事业分盘 D10 | 0.3 | 14.5 | 区间内会再换一次,这次判断是对的 |
| 九分盘 D9 | 8.517 | 7.917 | 区间内会换,这次判断是对的 |
「提前 0.3 分钟就换座」指的正是区间起点,也就是用户在选盘页已经选过的那条线。区间就是从这条线开始的,线后的盘是固定的。可是代码判断用的是 `before <= window.before_minutes`(0.3 ≤ 0.3),把这条线本身也算成了「区间内换座」。
## 要改的地方
1. `scripts/report_render/evidence_grade.py::_varga_boundary` 和 `scripts/report_render/birth_precision_margins.py`(生成 `summary_zh` 里「区间内会/不会换座」那一句)用同一条规则。抽成一个共享函数,两处都调用它。
2. 规则:只有换座点**严格落在区间内部**,才算区间内会换座。
- 前侧:`before < window.before_minutes - ε`
- 后侧:`after < window.after_minutes - ε`
- ε 取 0.01 分钟,用来吸收秒级的取整误差。在 PROGRESS 里写明为什么取这个值。
- 这条规则对 `rectified_range`、`user_stated` 和默认窗口都适用:换座点正好落在区间端点上,本来就不算区间内。
3. 已确认到分钟(±1 分钟)的旧规则不受影响,原有测试保持通过。
## 不要碰
- `frontend/**`
- `references/**`
- 选盘页本身(#229 负责)
- 冻结文件
## 验收(新测试)
1. 按上表构造合成事实包(`rectified_range`,前 0.3、后 15):
- D2、D6、D8 判为 `clear_at_tolerance`,`summary_zh` 写「区间内不换座」;
- D10、D9 判为 `boundary_sensitive`;
- `wealth_earning`、`wealth_retention`、`health_pressure` 的上限回到 C,`career_promise` 和 `relationship_structure` 仍然是 D。
2. 换座点比区间端点早 0.02 分钟(落在区间内)时,判为会换座。
3. #224、#227、#215 的测试全部通过。
## 自测命令
```
python3 -m pytest -q tests/test_report_birth_window_20261113.py tests/test_report_evidence_grade_20261112.py tests/test_evidence_grade_purpose_stability_20261112.py tests/test_report_reading_facts.py
python3 scripts/run_quality_gate.py --profile quick --skip-frontend-runtime
```
@@ -8,6 +8,8 @@ from domain_calculation_service import swiss_ephemeris_lock
from jyotish_engine import _birth_hour_decimal, _sidereal_calculation_profile, swe
from varga import calc_varga
from .birth_precision_window import crosses_birth_window
_CAP_SECONDS = 240 * 60
_STEP_SECONDS = 30
@@ -53,10 +55,7 @@ def _margin_summary_zh(before: float | None, after: float | None, cap: float, wi
else f"出生时间提前 {cap:g} 分钟内上升未换座")
later = (f"推后 {after:g} 分钟才换座" if after is not None
else f"推后 {cap:g} 分钟内未换座")
crosses = (
before is not None and 0 < window["before_minutes"] and before <= window["before_minutes"]
or after is not None and 0 < window["after_minutes"] and after <= window["after_minutes"]
)
crosses = crosses_birth_window(before, after, window)
conclusion = "区间内会换座" if crosses else "区间内不换座"
return f"{earlier};{later}。{conclusion}。"
@@ -7,6 +7,7 @@ import re
CONFIRMED_TOLERANCE_MINUTES = 1.0
DEFAULT_TOLERANCE_MINUTES = 30.0
_WINDOW_EDGE_TOLERANCE_MINUTES = 0.01
WINDOW_POLICY = "birth_record_window_v1"
_DECLARED_SOURCES = {"user_stated", "shichen", "period", "rectified_range"}
_MAX_SIDE_MINUTES = 720
@@ -64,6 +65,16 @@ def report_birth_window(value: object, *, minute_confirmed: bool) -> dict:
return result
def crosses_birth_window(before: float | None, after: float | None, window: dict) -> bool:
"""Check for a crossing in either direction, keeping the confirmed-minute rule inclusive."""
before_limit, after_limit = window["before_minutes"], window["after_minutes"]
if window.get("source") == "confirmed_minute":
return (before is not None and before <= before_limit
or after is not None and after <= after_limit)
return (before is not None and before < before_limit - _WINDOW_EDGE_TOLERANCE_MINUTES
or after is not None and after < after_limit - _WINDOW_EDGE_TOLERANCE_MINUTES)
def birth_window_label_zh(window: dict) -> str:
source = window["source"]
before, after = window["before_minutes"], window["after_minutes"]
+2 -3
View File
@@ -30,7 +30,7 @@ from __future__ import annotations
import re
from typing import Any
from .birth_precision_window import CONFIRMED_TOLERANCE_MINUTES, WINDOW_POLICY, report_birth_window
from .birth_precision_window import CONFIRMED_TOLERANCE_MINUTES, WINDOW_POLICY, crosses_birth_window, report_birth_window
try:
from scripts.consult_card_domain_facts import PRECISION_VARGA
@@ -165,8 +165,7 @@ def _varga_boundary(facts: dict, code: str) -> tuple[str, dict[str, Any]]:
before = float(margin["before_minutes"])
after = float(margin["after_minutes"])
detail.update(before_change_minutes=before, after_change_minutes=after, nearest_change_minutes=min(before, after))
crosses = (0 < window["before_minutes"] and before <= window["before_minutes"]
or 0 < window["after_minutes"] and after <= window["after_minutes"])
crosses = crosses_birth_window(before, after, window)
return ("boundary_sensitive" if crosses else "clear_at_tolerance"), detail
if not isinstance(margin, dict):
return "missing", {"policy": PRECISION_POLICY, "reason": "no ascendant margin for this varga"}
+45 -5
View File
@@ -2,7 +2,7 @@
from scripts.professional_report_reference import _reading_request
from scripts.report_render.birth_precision_margins import _margin_summary_zh
from scripts.report_render.evidence_grade import evidence_grades
from scripts.report_render.evidence_grade import _varga_boundary, evidence_grades
from scripts.report_render.reading_facts import _birth_precision
from tests.test_report_evidence_grade_20261112 import _facts
@@ -38,7 +38,7 @@ def test_default_window_checks_required_and_optional_vargas():
assert _row(claims, "health_pressure", "D30")["state"] == "not_evaluated"
def test_declared_window_uses_each_side_and_includes_the_endpoint():
def test_declared_window_uses_each_side_and_excludes_the_endpoint():
five = {"before_minutes": 5, "after_minutes": 5, "source": "user_stated"}
claims = evidence_grades(_unconfirmed_facts(five))["claims"]
assert claims["relationship_structure"]["cap"] == "C"
@@ -51,7 +51,7 @@ def test_declared_window_uses_each_side_and_includes_the_endpoint():
margin.update(before_minutes=0.3, after_minutes=21)
assert evidence_grades(facts)["claims"]["wealth_earning"]["cap"] == "C"
margin["after_minutes"] = 20
assert evidence_grades(facts)["claims"]["wealth_earning"]["cap"] == "D"
assert evidence_grades(facts)["claims"]["wealth_earning"]["cap"] == "C"
def test_shichen_request_reaches_facts_even_with_a_single_calculation_clock():
@@ -119,11 +119,51 @@ def test_rejected_rectified_range_stays_marked_for_intake_and_default():
def test_margin_summary_distinguishes_the_two_directions_and_search_cap():
assert _margin_summary_zh(0.3, 147, 240, {"before_minutes": 0.3, "after_minutes": 19}) == (
"出生时间提前 0.3 分钟上升就换座;推后 147 分钟才换座。区间内会换座。"
"出生时间提前 0.3 分钟上升就换座;推后 147 分钟才换座。区间内不换座。"
)
assert _margin_summary_zh(0.3, 147, 240, {"before_minutes": 0, "after_minutes": 19}) == (
"出生时间提前 0.3 分钟上升就换座;推后 147 分钟才换座。区间内不换座。"
)
assert _margin_summary_zh(None, 19, 240, {"before_minutes": 5, "after_minutes": 19}) == (
"出生时间提前 240 分钟内上升未换座;推后 19 分钟才换座。区间内会换座。"
"出生时间提前 240 分钟内上升未换座;推后 19 分钟才换座。区间内不换座。"
)
def test_rectified_window_only_caps_claims_for_crossings_inside_the_range():
window = {"before_minutes": 0.3, "after_minutes": 15, "source": "rectified_range"}
facts = _unconfirmed_facts(window)
margins = facts["birth_precision"]["ascendant_margins"]
for code, before, after in (
("D2", 0.3, 147.167), ("D6", 0.3, 24.367), ("D8", 0.3, 18.2),
("D10", 0.3, 14.5), ("D9", 8.517, 7.917),
):
margins[code] = {"before_minutes": before, "after_minutes": after}
claims = evidence_grades(facts)["claims"]
for code in ("D2", "D6", "D8", "D10", "D9"):
crosses = code in ("D10", "D9")
assert _varga_boundary(facts, code)[0] == ("boundary_sensitive" if crosses else "clear_at_tolerance")
margin = margins[code]
summary = _margin_summary_zh(margin["before_minutes"], margin["after_minutes"], 240, window)
assert summary.endswith("区间内会换座。" if crosses else "区间内不换座。")
assert {name: claims[name]["cap"] for name in (
"wealth_earning", "wealth_retention", "health_pressure", "career_promise", "relationship_structure",
)} == {"wealth_earning": "C", "wealth_retention": "C", "health_pressure": "C",
"career_promise": "D", "relationship_structure": "D"}
def test_window_edge_tolerance_applies_on_each_side_and_preserves_confirmed_minute():
for source, width in (("user_stated", 5), ("default_unknown_30", 30), ("rectified_range", 15), ("confirmed_minute", 1)):
window = {"before_minutes": width, "after_minutes": width, "source": source}
facts = _unconfirmed_facts(window)
facts["minute_confirmed"] = source == "confirmed_minute"
for offset, unconfirmed_crosses, confirmed_crosses in (
(0.02, False, False), (0, False, True), (-0.005, False, True), (-0.01, False, True), (-0.02, True, True),
):
crosses = confirmed_crosses if facts["minute_confirmed"] else unconfirmed_crosses
for before, after in ((width + offset, 73), (73, width + offset)):
facts["birth_precision"]["ascendant_margins"]["D2"] = {"before_minutes": before, "after_minutes": after}
assert _varga_boundary(facts, "D2")[0] == ("boundary_sensitive" if crosses else "clear_at_tolerance")
summary = _margin_summary_zh(before, after, 240, window)
assert summary.endswith("区间内会换座。" if crosses else "区间内不换座。")
+1 -1
View File
@@ -83,7 +83,7 @@ def test_unconfirmed_birth_time_at_default_boundary_is_not_assumed_stable():
facts = _facts(minute_confirmed=False)
claim = claim_grade(facts, "career_promise")
assert claim["ledger"]["purpose_stability"] == "not_evaluated"
assert claim["cap"] == "D"
assert claim["cap"] == "C"
def test_parameter_sensitive_timing_is_flagged_not_hidden():