diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d57072b..2e97441a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # 印度占星 Skill 更新日志 +## 2026-09-06 — 专业长报告补上逐月 KP 标题和校时分钟表 + +专业长报告里,事业、财务、关系三条逐月 MD/AD 支持只要有月份数据就会带标题;时间轨道来源改成可读标签,不再被脱敏成空。未确认出生分钟时,校时附录会列出窗口内候选分钟,不会写成已经采用的时间。Skill 版本未变。 + ## 2026-09-06 — 首轮咨询标题在刷新前就会落库 新开的普通咨询,模型起的主题标题会写进服务器。生成中途刷新或换设备,侧栏仍是那句总结,而不是问题前几个字加省略号。你中途自己改名的,仍以你改的为准。Skill 版本未变。 diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 8c2ed5ec..3325b1f4 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -8677,4 +8677,21 @@ - 复发自:无 - 修复版本:`ebc8380f` +## BUG-564 | 长报告 heading-ensure 之后仍丢逐月 KP 标题、时间来源与分钟排行 + +- 状态:resolved +- 首次发现:2026-09-06 +- 最近更新:2026-09-06 +- 影响面:`scripts/jyotish_engine.py` pl9 长报告、`scripts/flexible_birth_time_report_section.py`、`scripts/run_quality_gate.py` 快速门 +- 用户现象:有 KP 月份数据时,事业/财务/关系三条「逐月 MD/AD 支持」标题仍不见;时间节点归因表的来源列被脱敏成空;校时附录没有分钟排行。快速门也不跑 gaps2 合同。 +- 触发条件:`kp_monthly_report.months` 非空;professional 路径 sanitize 掉 `source_path`;候选窗存在但渲染层不读 `candidate_times`。 +- 根因:`_kp_monthly_theme_brief` 成功路径不写标题,空包才靠文末 heading-ensure 补 blocked 标题;parity_gates 用 `source_path`,sanitize 会剥掉任何 `_path` 键;CLI provenance 只读显式 `uncertainty_minutes`;分钟排行未装配。顺带 `d1_d60_ledger` / `support_topics.kp` 为 None 时 `.get` 会在有月份数据的渲染里炸掉。 +- 修复:逐月 brief 始终输出标题,空数据走 blocked;parity_gates 改 `source_label`;窗口宽度回填 uncertainty;校时附录列出候选分钟;None 守卫;快速门列入 gaps2 / parity / timing / flexible 合同。 +- 验证:`tests/test_report_longform_gaps2.py`、`tests/test_flexible_birth_time_report_contract.py`、`tests/test_timing_precision_contract.py`。 +- 防复发:有月份数据时三条逐月标题必须各出现一次,且不得靠文末 blocked 标题顶替;sanitize 后 Markdown 仍须保留可读 source_label;快速门必须跑上述四个测试文件。 +- 相关记录:BUG-561、BUG-562 +- 复发自:BUG-561(`cfcd369d` 的 heading-ensure 只覆盖空包缺席,未覆盖成功路径漏标题) +- 修复版本:待本提交 SHA + + diff --git a/docs/tasks/PROGRESS-report-longform-gaps2-20260906.md b/docs/tasks/PROGRESS-report-longform-gaps2-20260906.md index e8935ad2..6b6ce26a 100644 --- a/docs/tasks/PROGRESS-report-longform-gaps2-20260906.md +++ b/docs/tasks/PROGRESS-report-longform-gaps2-20260906.md @@ -39,3 +39,18 @@ 本 worktree 无独立 `.venv`,实测用父仓解释器。实现已随 `809bdf13` 快进到 `origin/staging`;门禁因 MD 页 lint(BUG-563)未部署,与 gaps2 装配无关。 未改 `.gitea/workflows/**`,不提升 main。 + +## 补洞(相对 `cfcd369d` / `5aa05bf0`) + +并行 worktree 在旧基线上多做的实现没有整棵 rebase(`jyotish_engine.py` 与 MD 页提交重叠太大)。在当前 staging HEAD 上只移植仍缺的洞: + +| 洞 | 处理 | +| --- | --- | +| 有月份时逐月 MD/AD 标题仍缺 | `_kp_monthly_theme_brief` 始终写 `#### KP {事业\|财务\|关系}逐月 MD/AD 支持`;空数据 blocked | +| sanitize 抹掉 `source_path` | parity_gates 改 `source_label` | +| 快速门未跑 gaps2 | `CORE_PYTEST_TARGETS` 列入 gaps2 / parity / timing / flexible | +| 校时附录无分钟排行 | `flexible_birth_time_report_section` 列出 `candidate_times`,禁止 winner/approved | +| CLI provenance 只认显式分钟 | 从 `candidate_range` / `declared_window_*` 回填 11:50–12:10 → 20 | +| Vargas I / KP 三年节 None.get | `d1_d60_ledger` 与 `support_topics.kp` 做 dict 守卫 | + +Muntha `conflict`、`pyjhora_behavior_only`、事件回放 `user_history_not_supplied` 保持原样。未 vendor `professional_parity_closure`。 diff --git a/docs/tasks/README.md b/docs/tasks/README.md index e5a0244d..421d5070 100644 --- a/docs/tasks/README.md +++ b/docs/tasks/README.md @@ -92,6 +92,8 @@ | `TASK-report-candidate-range-read-20260904.md`(仓库根) | `PROGRESS-report-candidate-range-20260904.md` | accepted 生时报告直读已收权校正表 | 待验收 | `e27d5dc5`(BUG-526/534;终稿见 BUG-535) | | `TASK-report-chart-cap-20260904.md`(仓库根) | `PROGRESS-report-chart-cap-20260904.md` | 文档 charts 上限绑定 CHART_IDS | 待验收 | `4ef4c406`(BUG-535) | | `TASK-report-longform-parity-20260905.md`(仓库根) | `PROGRESS-report-longform-parity-20260905.md` | 长报告对齐全量版并挂入产品附录 | 待验收 | `codex/report-longform-parity-20260905`(任务 6 待部署后核对) | +| `TASK-report-longform-gaps2-20260906.md`(仓库根) | `PROGRESS-report-longform-gaps2-20260906.md` | 长报告真实参数组合下的装配缺口 | 待验收 | `codex/report-longform-gaps2-20260906`(BUG-561/562;补洞 BUG-564) | +| `TASK-report-md-page-20260906.md` | `PROGRESS-report-md-page-20260906.md` | 长报告 Markdown 直接作为报告页 | 已合入 | `cfcd369d` / `809bdf13`(BUG-563 lint 随后修) | ### 前端基础与工程 diff --git a/scripts/flexible_birth_time_report_section.py b/scripts/flexible_birth_time_report_section.py index a8350f66..4f670263 100644 --- a/scripts/flexible_birth_time_report_section.py +++ b/scripts/flexible_birth_time_report_section.py @@ -35,6 +35,21 @@ def render_flexible_birth_time_report_section(projection: Mapping[str, Any]) -> lines.extend(["", "#### 窗口内敏感层"]) for key in sensitive.get("evidence_keys") or []: lines.append(f"- `{key}`:{_render_value((sensitive.get('layers') or {}).get(key))}") + lines.extend(["", "#### 校时附录·分钟排行", ""]) + candidate_times = window.get("candidate_times") if isinstance(window.get("candidate_times"), list) else [] + if candidate_times: + lines.extend([ + "下列分钟只是窗口内候选顺序,不是已确认出生分钟。", + "", + "| 序号 | 候选分钟 |", + "|------|----------|", + ]) + for idx, time in enumerate(candidate_times, start=1): + lines.append(f"| {idx} | {time} |") + else: + lines.extend([ + "`blocked` / `candidate_times_missing`", + ]) lines.extend([ "", "#### 使用边界", diff --git a/scripts/jyotish_engine.py b/scripts/jyotish_engine.py index 88e4c40e..d5ca81a8 100644 --- a/scripts/jyotish_engine.py +++ b/scripts/jyotish_engine.py @@ -2021,6 +2021,36 @@ def _attach_annual_tajika_pack(packet: dict, args) -> dict: return packet +def _uncertainty_minutes_from_args(args): + explicit = getattr(args, 'uncertainty_minutes', None) + if explicit not in (None, ''): + return explicit + before = getattr(args, 'uncertainty_before_minutes', None) + after = getattr(args, 'uncertainty_after_minutes', None) + if before not in (None, '') or after not in (None, ''): + try: + return int(before or 0) + int(after or 0) or None + except (TypeError, ValueError): + pass + candidate_range = getattr(args, 'candidate_range', None) or {} + start_raw = '' + end_raw = '' + if isinstance(candidate_range, dict): + start_raw = str(candidate_range.get('start_time') or '')[:5] + end_raw = str(candidate_range.get('end_time') or '')[:5] + if not start_raw: + start_raw = str(getattr(args, 'declared_window_start', None) or '')[:5] + if not end_raw: + end_raw = str(getattr(args, 'declared_window_end', None) or '')[:5] + try: + start = datetime.strptime(start_raw, '%H:%M') + end = datetime.strptime(end_raw, '%H:%M') + minutes = int((end - start).total_seconds() // 60) + return minutes if minutes >= 0 else None + except (TypeError, ValueError): + return None + + def _birth_provenance_from_args(args) -> dict: return { 'schema': 'jyotish.birth_provenance.v1', @@ -2028,7 +2058,7 @@ def _birth_provenance_from_args(args) -> dict: 'coordinate_precision': getattr(args, 'coordinate_precision', None) or 'unverified_user_coordinates', 'coordinate_source': getattr(args, 'coordinate_source', None) or 'not_recorded', 'time_source': getattr(args, 'time_source', None) or 'not_recorded', - 'uncertainty_minutes': getattr(args, 'uncertainty_minutes', None), + 'uncertainty_minutes': _uncertainty_minutes_from_args(args), 'boundary': 'A hospital label is not treated as hospital-grade coordinates unless coordinate_source and coordinate_precision explicitly support it.', } @@ -2286,25 +2316,25 @@ def _build_timing_boundary_attribution(packet: dict) -> dict: { 'lane': 'Vimshottari boundary series', 'status': 'parameter_sensitive', - 'source_path': 'raw_full_reading.modules.dasha', + 'source_label': 'full-reading Dasha module', 'reason': 'Local boundary series is present; broader formal external boundary closure remains incomplete.', }, { 'lane': 'Narayana Dasha', 'status': 'parameter_sensitive', - 'source_path': 'raw_full_reading.modules.narayana_dasha', + 'source_label': 'full-reading Narayana module', 'reason': 'Local sequence is present, but parity and interpretation closure remain incomplete.', }, { 'lane': 'Patyayini / Tajika annual', 'status': 'parameter_sensitive', - 'source_path': 'worksheets.timing_and_predictive_systems.annual_tajika_series', + 'source_label': 'annual Tajika series', 'reason': 'Independent annual packs exist, but annual timing remains limited by parity and replay gates.', }, { 'lane': 'KP exact cusp', 'status': kp_status, - 'source_path': 'references/oracle/kp_exact_cusp_mainline_status_2026_08_22.json', + 'source_label': 'KP exact cusp oracle status', 'reason': kp_reason or 'Exact-cusp promotion gate remains closed.', }, ] @@ -2605,6 +2635,9 @@ PLANNED_LONGFORM_SECTION_HEADINGS = ( '#### KP 财务宫位核对(2 / 5 / 9 / 11 宫)', '#### KP 关系结构小表', '#### KP 关系宫位核对(2 / 7 / 11 宫)', + '#### KP 事业逐月 MD/AD 支持', + '#### KP 财务逐月 MD/AD 支持', + '#### KP 关系逐月 MD/AD 支持', '## 逐模块执行与结论追踪审计', '## 完整 Technique Audit Table', '#### Bhava Bala(本地三分量评分)', @@ -2615,6 +2648,7 @@ PLANNED_LONGFORM_SECTION_HEADINGS = ( '#### Kala Chakra 参考对齐', '## 原始模块索引与未下沉字段附录', '### Profile-aware Benchmark Boundary', + '#### 校时附录·分钟排行', ) @@ -4396,7 +4430,8 @@ def render_pl9_markdown(packet: dict) -> str: rendered.append(f"| {label} | {' | '.join(_md_cell(value) for value in values)} | parameter_sensitive |") rendered.append('') - d1_d60_section = full_report_pack.get('sections', {}).get('d1_d60_ledger') if isinstance(full_report_pack.get('sections'), dict) else {} + pack_sections = full_report_pack.get('sections') if isinstance(full_report_pack.get('sections'), dict) else {} + d1_d60_section = pack_sections.get('d1_d60_ledger') if isinstance(pack_sections.get('d1_d60_ledger'), dict) else {} d1_d60_ledger = d1_d60_section.get('d1_to_d60') if isinstance(d1_d60_section.get('d1_to_d60'), dict) else {} d1_d60_summary = d1_d60_section.get('summary') if isinstance(d1_d60_section.get('summary'), dict) else {} if d1_d60_ledger: @@ -5321,7 +5356,9 @@ def render_pl9_markdown(packet: dict) -> str: out.append(f"| {_md_cell(row.get('month'))} | {_md_cell(row.get('reason'))} | parameter_sensitive |") out.append('') kp_support_topic = support_topics.get('kp') if isinstance(support_topics, dict) else {} - kp_snapshot = source_pack_contracts.get('kp_support_snapshot') if isinstance(source_pack_contracts.get('kp_support_snapshot'), dict) else {} + if not isinstance(kp_support_topic, dict): + kp_support_topic = {} + kp_snapshot = source_pack_contracts.get('kp_support_snapshot') if isinstance(source_pack_contracts, dict) and isinstance(source_pack_contracts.get('kp_support_snapshot'), dict) else {} kp_must_not_claim = kp_support_topic.get('must_not_claim') if isinstance(kp_support_topic.get('must_not_claim'), list) else [] if not kp_must_not_claim: kp_must_not_claim = kp_snapshot.get('must_not_claim') if isinstance(kp_snapshot.get('must_not_claim'), list) else [] @@ -5376,11 +5413,21 @@ def render_pl9_markdown(packet: dict) -> str: def _kp_monthly_theme_brief(domain: str) -> list[str]: monthly_pack = advanced_sheet.get('kp_monthly_report') if isinstance(advanced_sheet.get('kp_monthly_report'), dict) else {} - months = monthly_pack.get('months') if isinstance(monthly_pack.get('months'), list) else [] - if not months: - return [] + if 'result' in monthly_pack and 'response_envelope' in monthly_pack: + nested = monthly_pack.get('result') + monthly_pack = nested if isinstance(nested, dict) else {} + raw_months = monthly_pack.get('months') + if isinstance(raw_months, list): + months = raw_months + elif isinstance(raw_months, dict): + months = [raw_months[key] for key in sorted(raw_months, key=str)] + else: + months = [] label_map = {'career': '事业', 'wealth': '财务', 'relationship': '关系'} label = label_map.get(domain, domain) + heading = f'#### KP {label}逐月 MD/AD 支持' + if not months: + return _blocked_planned_section(heading, 'kp_monthly_payload_empty') selected_rows = [] for row in months[:3]: if not isinstance(row, dict): @@ -5394,7 +5441,7 @@ def render_pl9_markdown(packet: dict) -> str: focus_code = detail.get('active_houses') or detail.get('houses') or detail.get('promise_code') selected_rows.append({'month': row.get('month'), 'dasha_pair': f"{_humanize_reader_token(md.get('lord'))} / {_humanize_reader_token(ad.get('lord'))}", 'summary': detail.get('brief_summary') or detail.get('summary') or '-', 'focus': _kp_house_focus_text(focus_code)}) if not selected_rows: - return [] + return _blocked_planned_section(heading, 'kp_monthly_theme_empty') month_names = '、'.join(str(row.get('month')) for row in selected_rows if row.get('month')) dasha_brief = _dasha_domain_brief(domain) focus_values = [] @@ -5403,7 +5450,11 @@ def render_pl9_markdown(packet: dict) -> str: if focus and focus != '-' and focus not in focus_values: focus_values.append(focus) focus_text = '、'.join(focus_values) - brief = [f"对应到月度层,{label}线优先先看 {month_names};这里仍只保留 parameter_sensitive 的结构提示,不把它写成具体事件预测。"] + brief = [ + heading, + '', + f"对应到月度层,{label}线优先先看 {month_names};这里仍只保留 parameter_sensitive 的结构提示,不把它写成具体事件预测。", + ] if focus_text: brief.extend(['', f'焦点宫位:{focus_text}']) if dasha_brief: @@ -9820,6 +9871,7 @@ def render_pl9_markdown(packet: dict) -> str: '未做校时。当前没有可用的候选窗,因此不输出分钟级敏感性矩阵或校时分钟排行。', '', ]) + lines.extend(_blocked_planned_section('#### 校时附录·分钟排行', 'candidate_window_not_applicable')) lines.extend(['', '## 校时证据领域合同', '']) lines.append(_md_cell(rectification_evidence_contract.get('claim_boundary', 'rectification_evidence_contract_missing'))) @@ -9853,13 +9905,7 @@ def render_pl9_markdown(packet: dict) -> str: lines.extend(['', '| Parity lane | 状态 | 来源路径 | 仍未闭环原因 |', '|-------------|------|----------|----------------|']) for item in timing_boundary_attribution.get('parity_gates', []) if isinstance(timing_boundary_attribution.get('parity_gates'), list) else []: if isinstance(item, dict): - source_path = str(item.get('source_path') or '') - source_label = { - 'raw_full_reading.modules.dasha': 'full-reading Dasha module', - 'raw_full_reading.modules.narayana_dasha': 'full-reading Narayana module', - 'worksheets.timing_and_predictive_systems.annual_tajika_series': 'annual Tajika series', - 'references/oracle/kp_exact_cusp_mainline_status_2026_08_22.json': 'KP exact cusp oracle status', - }.get(source_path, source_path.split('.')[-1] if source_path else '-') + source_label = str(item.get('source_label') or item.get('source') or '-') lines.append(f"| {_md_cell(item.get('lane'))} | {_md_cell(item.get('status'))} | {_md_cell(source_label)} | {_md_cell(item.get('reason'))} |") lines.extend(['', '## 完整 Technique Audit Table', '', '| 技法 | 状态 | 来源 | 审计说明 |', '|------|------|------|----------|']) diff --git a/scripts/run_quality_gate.py b/scripts/run_quality_gate.py index f503ad13..0c793996 100644 --- a/scripts/run_quality_gate.py +++ b/scripts/run_quality_gate.py @@ -81,6 +81,10 @@ CORE_PYTEST_TARGETS = [ "tests/test_consultation_workflow_domains.py", # Float hour/minute from the API payload must not 500 consultation_workflow (BUG-524). "tests/test_consultation_workflow_birth_time_sensitivity.py", + "tests/test_report_longform_parity.py", + "tests/test_report_longform_gaps2.py", + "tests/test_timing_precision_contract.py", + "tests/test_flexible_birth_time_report_contract.py", "tests/test_mcp_strict_workflow_finance.py", "tests/test_interpretation_template_registry.py", "tests/test_vedastro_external_technique_evidence.py", diff --git a/tests/test_flexible_birth_time_report_contract.py b/tests/test_flexible_birth_time_report_contract.py index a8938167..f12bbc81 100644 --- a/tests/test_flexible_birth_time_report_contract.py +++ b/tests/test_flexible_birth_time_report_contract.py @@ -62,7 +62,12 @@ def test_report_projection_is_hash_stable_and_renders_only_candidate_window_evid assert build_flexible_birth_time_full_report_projection(reordered)["projection_id"] == projection["projection_id"] assert projection["status"] == "candidate_window_only" - assert "出生时间敏感度" in render_flexible_birth_time_report_section(projection) + rendered = render_flexible_birth_time_report_section(projection) + assert "出生时间敏感度" in rendered + assert "#### 校时附录·分钟排行" in rendered + assert "08:00" in rendered + assert "winner" not in rendered.lower() + assert "approved" not in rendered.lower() @pytest.mark.parametrize("field", ["approved_birth_time", "final_birth_time", "winner", "approval"]) diff --git a/tests/test_report_longform_gaps2.py b/tests/test_report_longform_gaps2.py index a0dc54f4..b47607ad 100644 --- a/tests/test_report_longform_gaps2.py +++ b/tests/test_report_longform_gaps2.py @@ -8,6 +8,7 @@ from scripts.jyotish_engine import ( PLANNED_LONGFORM_SECTION_HEADINGS, _annual_series_surface, _attach_report_governance_contracts, + _birth_provenance_from_args, _native_dasha_master_families, render_pl9_markdown, sanitize_professional_report_reference, @@ -168,3 +169,81 @@ def test_auxiliary_dasha_headings_render_from_native_modules() -> None: assert "#### Narayana Rashi Dasha" in markdown assert "#### Kala Chakra Dasha(本地原始周期,冲突保留)" in markdown assert "#### Bhava Bala(本地三分量评分)" in markdown + + +def test_quality_gate_runs_this_file() -> None: + from scripts.run_quality_gate import CORE_PYTEST_TARGETS + + assert "tests/test_report_longform_gaps2.py" in CORE_PYTEST_TARGETS + assert "tests/test_report_longform_parity.py" in CORE_PYTEST_TARGETS + assert "tests/test_timing_precision_contract.py" in CORE_PYTEST_TARGETS + assert "tests/test_flexible_birth_time_report_contract.py" in CORE_PYTEST_TARGETS + + +def test_kp_monthly_theme_brief_keeps_heading_when_months_exist() -> None: + markdown = render_pl9_markdown(_producer_packet({ + "advanced_systems": { + "kp_monthly_report": { + "months": [ + { + "month": "2026-01", + "theme_support": { + "career": {"brief_summary": "career-support", "active_houses": [10]}, + "wealth": {"brief_summary": "wealth-support", "active_houses": [2]}, + "relationship": {"brief_summary": "relationship-support", "active_houses": [7]}, + }, + "vimshottari_five_levels": { + "levels": { + "mahadasha": {"lord": "Saturn"}, + "antardasha": {"lord": "Mercury"}, + } + }, + } + ] + } + } + })) + assert "#### KP 事业逐月 MD/AD 支持" in markdown + assert "#### KP 财务逐月 MD/AD 支持" in markdown + assert "#### KP 关系逐月 MD/AD 支持" in markdown + assert "career-support" in markdown + assert markdown.count("#### KP 事业逐月 MD/AD 支持") == 1 + assert "kp_monthly_payload_empty" not in markdown + assert "planned_section_unavailable" not in markdown.split("#### KP 事业逐月 MD/AD 支持", 1)[-1].split("#### KP 财务逐月 MD/AD 支持", 1)[0] + + +def test_sanitized_packet_keeps_timing_source_labels() -> None: + packet = _attach_report_governance_contracts( + _producer_packet({}), + SimpleNamespace(**BEIJING, birth_time_accuracy="confirmed"), + ) + sanitized = sanitize_professional_report_reference(packet) + markdown = render_pl9_markdown(sanitized) + assert "full-reading Dasha module" in markdown + assert "full-reading Narayana module" in markdown + assert "annual Tajika series" in markdown + assert "KP exact cusp oracle status" in markdown + assert "raw_full_reading.modules" not in markdown + assert "references/oracle" not in markdown + + +def test_birth_provenance_uses_candidate_window_minutes() -> None: + provenance = _birth_provenance_from_args(SimpleNamespace( + birthplace_label="测试市", + coordinate_source="user_reported", + coordinate_precision="unverified_user_coordinates", + time_source="user_reported", + candidate_range={"start_time": "11:50", "end_time": "12:10"}, + )) + assert provenance["uncertainty_minutes"] == 20 + assert provenance["coordinate_source"] == "user_reported" + + +def test_birth_provenance_uses_declared_window_minutes() -> None: + provenance = _birth_provenance_from_args(SimpleNamespace( + declared_window_start="11:50", + declared_window_end="12:10", + coordinate_source="user_reported", + )) + assert provenance["uncertainty_minutes"] == 20 + assert provenance["coordinate_source"] == "user_reported" diff --git a/tests/test_timing_precision_contract.py b/tests/test_timing_precision_contract.py index 34d136db..e9ebf238 100644 --- a/tests/test_timing_precision_contract.py +++ b/tests/test_timing_precision_contract.py @@ -26,6 +26,12 @@ def test_timing_contract_preserves_dates_but_caps_claim_status() -> None: assert contract["promotion_gate"]["status"] == "blocked" +def test_quality_gate_runs_this_file() -> None: + from scripts.run_quality_gate import CORE_PYTEST_TARGETS + + assert "tests/test_timing_precision_contract.py" in CORE_PYTEST_TARGETS + + def test_empty_timing_contract_still_exposes_boundary() -> None: contract = build_timing_precision_contract() assert contract["timing_precision"] == "broad_window_only"