fix(report): keep monthly KP headings and readable timing sources

Heading-ensure only covered empty packets, so present KP months still dropped MD/AD titles and sanitizer blanked source_path. Also lock the quality gate onto the new contracts.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-09-06 23:00:01 +08:00
parent 5aa05bf0d0
commit e4d16b7545
10 changed files with 213 additions and 20 deletions
+4
View File
@@ -1,5 +1,9 @@
# 印度占星 Skill 更新日志
## 2026-09-06 — 专业长报告补上逐月 KP 标题和校时分钟表
专业长报告里,事业、财务、关系三条逐月 MD/AD 支持只要有月份数据就会带标题;时间轨道来源改成可读标签,不再被脱敏成空。未确认出生分钟时,校时附录会列出窗口内候选分钟,不会写成已经采用的时间。Skill 版本未变。
## 2026-09-06 — 首轮咨询标题在刷新前就会落库
新开的普通咨询,模型起的主题标题会写进服务器。生成中途刷新或换设备,侧栏仍是那句总结,而不是问题前几个字加省略号。你中途自己改名的,仍以你改的为准。Skill 版本未变。
+17
View File
@@ -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 始终输出标题,空数据走 blockedparity_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
@@ -39,3 +39,18 @@
本 worktree 无独立 `.venv`,实测用父仓解释器。实现已随 `809bdf13` 快进到 `origin/staging`;门禁因 MD 页 lintBUG-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:5012: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`
+2
View File
@@ -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 随后修) |
### 前端基础与工程
@@ -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([
"",
"#### 使用边界",
+65 -19
View File
@@ -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', '', '| 技法 | 状态 | 来源 | 审计说明 |', '|------|------|------|----------|'])
+4
View File
@@ -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",
@@ -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"])
+79
View File
@@ -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"
+6
View File
@@ -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"