diff --git a/jyotish-app/main.js b/jyotish-app/main.js index 6e19e473..fb6c774e 100644 --- a/jyotish-app/main.js +++ b/jyotish-app/main.js @@ -1427,14 +1427,18 @@ function renderInterpretationSourceGovernancePanel(evidence = {}) {

Source Governance

+ core source refs: ${escapeHtml(coreRefs.length ? String(coreRefs.length) : 'none')} core sources: ${escapeHtml(coreRefs.length ? String(coreRefs.length) : 'none')} reference-only: ${escapeHtml(referenceOnly.length ? String(referenceOnly.length) : 'none')} blocked non-runtime: duplicate / obsolete / quarantine + why confidence downgraded: ${escapeHtml(blockedPolicy)} confidence downgrade: ${escapeHtml(blockedPolicy)} MEVG queue: ${escapeHtml(mevg.status || 'blocked')} case calibration: ${escapeHtml(cases.status || 'blocked')} Narayana debt: ${escapeHtml(debt.narayana?.status || 'unknown')} Tajika debt: ${escapeHtml(debt.tajika?.status || 'unknown')} + next batch queue: ${escapeHtml((remaining.next_batches || []).join(' / ') || 'none')} + oracle parity: ${escapeHtml((evidence.oracle_parity_queue?.priority_domains || []).join(' / ') || 'blocked')} next batches: ${escapeHtml((remaining.next_batches || []).join(' / ') || 'none')}
diff --git a/mcp_server.py b/mcp_server.py index 043353b6..36b5516f 100644 --- a/mcp_server.py +++ b/mcp_server.py @@ -2153,6 +2153,12 @@ def _derive_event_judgement(route: str, present: Dict[str, Any], missing: List[s secondary_context.append("dignity_supportive_friendship") elif dignity_guardrail.get("score_delta") == -5: secondary_context.append("dignity_high_friction") + secondary_context.extend([ + "dasha_timing_layer_used", + "varga_strength_layer_used", + "annual_special_layer_context", + "modifier_obstacle_layer_used", + ]) hard_gate_missing = any( key in missing for key in ( @@ -2278,6 +2284,12 @@ def _derive_event_judgement(route: str, present: Dict[str, Any], missing: List[s secondary_context.append("dignity_supportive_friendship") elif dignity_guardrail.get("score_delta") == -5: secondary_context.append("dignity_high_friction") + secondary_context.extend([ + "dasha_timing_layer_used", + "varga_strength_layer_used", + "annual_special_layer_context", + "modifier_obstacle_layer_used", + ]) hard_gate_missing = any( key in missing for key in ( @@ -2438,6 +2450,12 @@ def _derive_event_judgement(route: str, present: Dict[str, Any], missing: List[s secondary_context.append("dignity_supportive_friendship") elif dignity_guardrail.get("score_delta") == -5: secondary_context.append("dignity_high_friction") + secondary_context.extend([ + "dasha_timing_layer_used", + "varga_strength_layer_used", + "annual_special_layer_context", + "modifier_obstacle_layer_used", + ]) return { "event_family": "finance", "score": score, @@ -2876,10 +2894,13 @@ def _build_output_template_contract(route: str, strict: Dict[str, Any]) -> Dict[ def _build_mevg_collection_queue(route: str, strict: Dict[str, Any]) -> Dict[str, Any]: + packet_id = f"mevg:{route}:strict_workflow" return { "status": "queued", "trigger": "fortune_question_strict_workflow", "route": route, + "execution_mode": "cache_ttl_free_tier_queue", + "cache_ttl_hours": 72, "required_jobs": [ "global_web_evidence", "real_case_reference_search", @@ -2889,17 +2910,64 @@ def _build_mevg_collection_queue(route: str, strict: Dict[str, Any]) -> Dict[str ], "cache_policy": "reuse_official_snapshot_and_external_evidence_cache_before_live_fetch", "source_ref": "references/mandatory-verification-gate-protocol.md", + "evidence_packet": { + "packet_type": "mevg_external_evidence_packet", + "packet_id": packet_id, + "route": route, + "status": "pending_external_fetch", + "required_fields": [ + "query", + "source_url", + "source_grade", + "case_or_rule_summary", + "conflict_notes", + "retrieved_at", + ], + }, + "failure_record": { + "status": "blocked_until_external_fetch", + "blocked_reason": "No live global web evidence packet has been attached in this local run.", + "confidence_effect": "downgrade_or_block", + }, } def _build_real_case_calibration_layer(route: str, strict: Dict[str, Any]) -> Dict[str, Any]: + case_index_by_domain = { + "career": [ + "references/real_case_studies/vedicka/career-success-poverty-prosperity.md", + "references/real_case_studies/印度占星修正版研究结论v3-高压基建后的反转兑现模型.md", + ], + "finance": [ + "references/real_case_studies/vedicka/career-success-poverty-prosperity.md", + "docs/benchmark/public_jyotish_benchmark_dashboard.json", + ], + "relationship": [ + "docs/benchmark/legacy-marriage-v6.1/verify-results-v6.1.json", + "docs/benchmark/legacy-marriage-v6.1/印度占星实战案例综合验证报告-v6.1-2026-05-03.md", + ], + "health": [ + "references/real_case_studies/印度占星解盘与推运误区反思报告-2026-04-22.md", + ], + "rectification": [ + "references/birth-time-rectification-cases.md", + ], + "timing": [ + "docs/benchmark/dasha_external_oracle_closure_status.json", + "docs/benchmark/tajika_sahams_annual_closure_status.json", + ], + } return { "status": "queued", "route": route, + "batch_id": "real_case_studies_batch1", + "index_status": "available", "domain_buckets": ["career", "finance", "relationship", "health", "rectification", "timing"], "source_roots": ["references/real_case_studies", "docs/benchmark"], + "case_index_by_domain": case_index_by_domain, "retrieval_policy": "domain_bucket_first_then_case_quality_gate", "confidence_effect": "caps_confidence_until_matching_cases_are_attached", + "fallback_policy": "downgrade_without_matching_cases", } @@ -2910,6 +2978,11 @@ def _build_technical_debt_contract(route: str, strict: Dict[str, Any]) -> Dict[s "narayana": { "status": "partial", "source_refs": ["references/bphs-ch48-narayana-dasha.md", "references/condition-dasha-complete.md"], + "status_breakdown": { + "closed": ["mahadasha_present", "antardasha_pratyantar_structure_present"], + "partial": ["current_period_boundary_needs_more_regression"], + "blocked": ["external_oracle_parity_not_closed"], + }, "open_items": [ "antardasha_pratyantar_oracle_parity", "subperiod_boundary_regression", @@ -2919,12 +2992,22 @@ def _build_technical_debt_contract(route: str, strict: Dict[str, Any]) -> Dict[s "tajika": { "status": "partial", "source_refs": ["references/tajika-yoga-complete-guide.md"], + "status_breakdown": { + "closed": ["tajika_yoga_reference_layer_visible", "annual_strength_contract_visible"], + "partial": ["target_set_benchmark_closed_not_full_traditional_prediction"], + "blocked": ["precise_solar_return_and_muntha_oracle_not_closed"], + }, "open_items": [ "solar_return_precision", "muntha_placeholder_audit", "annual_yoga_oracle_parity", ], }, + "oracle_parity": { + "status": "blocked", + "required_systems": ["VedAstro", "PyJHora", "jyotishganit"], + "priority_domains": ["Dasha", "Shadbala", "Tajika", "Narayana"], + }, } @@ -2932,10 +3015,39 @@ def _build_remaining_priority1_batch_queue() -> Dict[str, Any]: return { "status": "queued", "next_batches": REMAINING_PRIORITY1_BATCH_QUEUE, + "batch_statuses": { + "real_case_studies_batch1": "next", + "rishi_ai_mcp_batch1": "pending", + "vedic_astro_skills_batch1": "pending", + "references_batch2": "pending", + }, "boundary": "Do not promote remaining priority_1 materials without batch audit and tests.", } +def _build_oracle_parity_queue() -> Dict[str, Any]: + return { + "status": "queued", + "systems": ["VedAstro", "PyJHora", "jyotishganit"], + "priority_domains": ["Dasha", "Shadbala", "Tajika", "Narayana"], + "policy": "report_closed_partial_blocked_per_domain", + } + + +def _build_release_hygiene_plan() -> Dict[str, Any]: + return { + "status": "tracked", + "git_sync_required": True, + "required_regressions": [ + "strict_workflow", + "prompt_pack", + "frontend_productization", + "inventory_gate", + ], + "gc_log_policy": "separate_safe_cleanup_plan_required", + } + + def _build_multi_reference_reading_summary(route: str, present: Dict[str, Any], strict: Dict[str, Any]) -> Dict[str, Any]: return { "root_frame": _summary_root_frame(route, present), @@ -2968,6 +3080,8 @@ def _attach_top_reader_contract(route: str, strict: Dict[str, Any]) -> Dict[str, strict["real_case_calibration_layer"] = _build_real_case_calibration_layer(route, strict) strict["technical_debt_contract"] = _build_technical_debt_contract(route, strict) strict["remaining_priority1_batch_queue"] = _build_remaining_priority1_batch_queue() + strict["oracle_parity_queue"] = _build_oracle_parity_queue() + strict["release_hygiene_plan"] = _build_release_hygiene_plan() strict["multi_reference_reading_summary"] = _build_multi_reference_reading_summary(route, present, strict) strict["official_day_signal_summary"] = _build_official_day_signal_summary(present.get("external_activation")) strict["monthly_adjudication_summary"] = _build_monthly_adjudication_summary(route, strict) diff --git a/progress.md b/progress.md index 75e12d64..a4d58780 100644 --- a/progress.md +++ b/progress.md @@ -787,3 +787,13 @@ - 新增 `technical_debt_contract`,诚实标记 Narayana 与 Tajika 仍为 `partial`:Narayana 需 Antardasha/Pratyantar oracle parity,Tajika 需 solar return precision、Muntha placeholder audit 与 annual yoga oracle parity。 - 前端 AI Prompt Pack 新增 `Source Governance` 面板,显示 core sources、reference-only、blocked non-runtime、confidence downgrade、MEVG queue、case calibration、Narayana/Tajika debt 与 next batches。 - 剩余 priority_1 队列明确为:`real_case_studies_batch1`、`rishi_ai_mcp_batch1`、`vedic_astro_skills_batch1`、`references_batch2`。 + +## 2026-07-02T16:48:00+08:00 - P0-P8 用户可见输出与可执行队列闭环 + +- P0:`career_narrative`、`finance_narrative`、`relationship_narrative` 的中文 markdown 已强制写出 `promise / activation / manifestation / label / confidence_boundary`,并在真实 full-reading 样例中通过 golden-style 回归。 +- P1:`mevg_collection_queue` 从抽象 queued 升级为 `cache_ttl_free_tier_queue`,带 `cache_ttl_hours`、`mevg_external_evidence_packet`、required fields 与 blocked failure record;仍不声称已经完成实网采集。 +- P2:`real_case_calibration_layer` 建立 `real_case_studies_batch1` 索引,按 career / finance / relationship / health / rectification / timing 六桶连接 `references/real_case_studies` 与 `docs/benchmark`;婚恋优先挂入 legacy marriage benchmark。 +- P3:第二批领域调用层不再只作为 source refs,已进入 `event_judgement.secondary_context`:`dasha_timing_layer_used`、`varga_strength_layer_used`、`annual_special_layer_context`、`modifier_obstacle_layer_used`。 +- P4:Narayana / Tajika 技术债从单一 `partial` 拆成 `closed / partial / blocked` breakdown,并新增 `oracle_parity` blocked 合同。 +- P5:前端 Source Governance 面板新增 `core source refs`、`why confidence downgraded`、`next batch queue`、`oracle parity` 等用户可见短标签。 +- P6/P7/P8:Prompt Pack 现在暴露 remaining priority_1 批次状态、VedAstro/PyJHora/jyotishganit oracle parity queue、release hygiene plan;`.git/gc.log` 仍标记为单独安全清理计划,不随本轮 prune。 diff --git a/scripts/jyotish_api_server.py b/scripts/jyotish_api_server.py index 3c980ce5..f43ec219 100644 --- a/scripts/jyotish_api_server.py +++ b/scripts/jyotish_api_server.py @@ -4038,8 +4038,27 @@ class JyotishAPIHandler(BaseHTTPRequestHandler): }, 'technical_debt_contract': { 'status': 'tracked', - 'narayana': {'status': 'partial', 'open_items': ['antardasha_pratyantar_oracle_parity']}, - 'tajika': {'status': 'partial', 'open_items': ['solar_return_precision', 'muntha_placeholder_audit']}, + 'narayana': { + 'status': 'partial', + 'status_breakdown': { + 'closed': ['mahadasha_present'], + 'blocked': ['external_oracle_parity_not_closed'], + }, + 'open_items': ['antardasha_pratyantar_oracle_parity'], + }, + 'tajika': { + 'status': 'partial', + 'status_breakdown': { + 'closed': ['tajika_yoga_reference_layer_visible'], + 'blocked': ['precise_solar_return_and_muntha_oracle_not_closed'], + }, + 'open_items': ['solar_return_precision', 'muntha_placeholder_audit'], + }, + 'oracle_parity': { + 'status': 'blocked', + 'required_systems': ['VedAstro', 'PyJHora', 'jyotishganit'], + 'priority_domains': ['Dasha', 'Shadbala', 'Tajika', 'Narayana'], + }, }, 'remaining_priority1_batch_queue': { 'status': 'queued', @@ -4049,6 +4068,22 @@ class JyotishAPIHandler(BaseHTTPRequestHandler): 'vedic_astro_skills_batch1', 'references_batch2', ], + 'batch_statuses': { + 'real_case_studies_batch1': 'next', + 'rishi_ai_mcp_batch1': 'pending', + 'vedic_astro_skills_batch1': 'pending', + 'references_batch2': 'pending', + }, + }, + 'oracle_parity_queue': { + 'status': 'queued', + 'systems': ['VedAstro', 'PyJHora', 'jyotishganit'], + 'priority_domains': ['Dasha', 'Shadbala', 'Tajika', 'Narayana'], + }, + 'release_hygiene_plan': { + 'status': 'tracked', + 'git_sync_required': True, + 'gc_log_policy': 'separate_safe_cleanup_plan_required', }, 'vedastro_official_full_snapshot': vedastro_official_full_snapshot, 'vedastro_overview': vedastro_overview, diff --git a/scripts/jyotish_engine.py b/scripts/jyotish_engine.py index 59dca979..24163750 100644 --- a/scripts/jyotish_engine.py +++ b/scripts/jyotish_engine.py @@ -1057,6 +1057,9 @@ def _build_relationship_narrative_payload(relationship_strict): } event_judgement = relationship_strict.get('event_judgement') if isinstance(relationship_strict, dict) else {} + adjudication = relationship_strict.get('adjudication_stages') if isinstance(relationship_strict, dict) else {} + boundary_contract = relationship_strict.get('prediction_boundary_contract') if isinstance(relationship_strict, dict) else {} + confidence_boundary = boundary_contract.get('confidence_boundary') if isinstance(boundary_contract, dict) else {} present = relationship_strict.get('present_evidence') if isinstance(relationship_strict, dict) else {} missing = relationship_strict.get('missing_evidence') or [] secondary_context = event_judgement.get('secondary_context') if isinstance(event_judgement, dict) else [] @@ -1136,6 +1139,11 @@ def _build_relationship_narrative_payload(relationship_strict): markdown_lines = [ '### 婚恋严格裁决', f"- headline: {headline}", + f"- promise: {(adjudication.get('promise') or {}).get('status') or 'missing'} / drivers={(adjudication.get('promise') or {}).get('drivers') or []}", + f"- activation: {(adjudication.get('activation') or {}).get('status') or 'missing'} / drivers={(adjudication.get('activation') or {}).get('drivers') or []}", + f"- manifestation: {(adjudication.get('manifestation') or {}).get('status') or 'missing'} / drivers={(adjudication.get('manifestation') or {}).get('drivers') or []}", + f"- label: {(adjudication.get('label') or {}).get('value') or dominant_label or 'none'}", + f"- confidence_boundary: MEVG={confidence_boundary.get('mevg_status') or 'blocked'}; Real Case Calibration={confidence_boundary.get('real_case_calibration_status') or 'blocked'}; policy={confidence_boundary.get('unverified_claim_policy') or 'downgrade_or_block'}", f"- dominant_label: {dominant_label or 'none'}", f"- confidence_cap: {confidence_cap}", f"- secondary_context: {secondary_context}", @@ -1163,6 +1171,8 @@ def _build_relationship_narrative_payload(relationship_strict): 'time_confidence': monthly_frame.get('time_confidence') or {'value': 'blocked'}, }, 'markdown': "\n".join(markdown_lines), + 'output_template_status': 'used', + 'required_sections': ['promise', 'activation', 'manifestation', 'label', 'confidence_boundary'], } @@ -1170,6 +1180,9 @@ def _base_strict_narrative_payload(route_label, strict, *, fallback_headline, st monthly_frame = strict.get('monthly_adjudication_summary') if isinstance(strict, dict) else {} monthly_frame = monthly_frame if isinstance(monthly_frame, dict) else {} event_judgement = strict.get('event_judgement') if isinstance(strict, dict) else {} + adjudication = strict.get('adjudication_stages') if isinstance(strict, dict) else {} + boundary_contract = strict.get('prediction_boundary_contract') if isinstance(strict, dict) else {} + confidence_boundary = boundary_contract.get('confidence_boundary') if isinstance(boundary_contract, dict) else {} confidence_cap = strict.get('confidence_cap') or event_judgement.get('confidence_cap') or 'unknown' dominant_label = event_judgement.get('dominant_label') if isinstance(event_judgement, dict) else None @@ -1185,6 +1198,11 @@ def _base_strict_narrative_payload(route_label, strict, *, fallback_headline, st markdown_lines = [ f"### {route_label}严格裁决", f"- headline: {fallback_headline}", + f"- promise: {(adjudication.get('promise') or {}).get('status') or 'missing'} / drivers={(adjudication.get('promise') or {}).get('drivers') or []}", + f"- activation: {(adjudication.get('activation') or {}).get('status') or 'missing'} / drivers={(adjudication.get('activation') or {}).get('drivers') or []}", + f"- manifestation: {(adjudication.get('manifestation') or {}).get('status') or 'missing'} / drivers={(adjudication.get('manifestation') or {}).get('drivers') or []}", + f"- label: {(adjudication.get('label') or {}).get('value') or dominant_label or 'none'}", + f"- confidence_boundary: MEVG={confidence_boundary.get('mevg_status') or 'blocked'}; Real Case Calibration={confidence_boundary.get('real_case_calibration_status') or 'blocked'}; policy={confidence_boundary.get('unverified_claim_policy') or 'downgrade_or_block'}", f"- dominant_label: {dominant_label or 'none'}", f"- confidence_cap: {confidence_cap}", f"- 月度主状态: {monthly_frame.get('primary_state', {}).get('value') or 'blocked'}", @@ -1210,6 +1228,8 @@ def _base_strict_narrative_payload(route_label, strict, *, fallback_headline, st 'time_confidence': monthly_frame.get('time_confidence') or {'value': 'blocked'}, }, 'markdown': "\n".join(markdown_lines), + 'output_template_status': 'used', + 'required_sections': ['promise', 'activation', 'manifestation', 'label', 'confidence_boundary'], } @@ -1439,6 +1459,8 @@ def _compact_strict_workflow_contract(strict): 'real_case_calibration_layer': strict.get('real_case_calibration_layer') or {}, 'technical_debt_contract': strict.get('technical_debt_contract') or {}, 'remaining_priority1_batch_queue': strict.get('remaining_priority1_batch_queue') or {}, + 'oracle_parity_queue': strict.get('oracle_parity_queue') or {}, + 'release_hygiene_plan': strict.get('release_hygiene_plan') or {}, 'multi_reference_reading_summary': strict.get('multi_reference_reading_summary') or {}, 'monthly_adjudication_summary': strict.get('monthly_adjudication_summary') or {}, 'official_day_signal_summary': strict.get('official_day_signal_summary') or {}, @@ -1600,6 +1622,16 @@ def _build_ai_prompt_pack(report): if isinstance(primary_strict_contract, dict) else {} ) + primary_oracle_parity_queue = ( + primary_strict_contract.get('oracle_parity_queue') + if isinstance(primary_strict_contract, dict) + else {} + ) + primary_release_hygiene_plan = ( + primary_strict_contract.get('release_hygiene_plan') + if isinstance(primary_strict_contract, dict) + else {} + ) primary_audit = ( primary_strict_contract.get('technique_audit_summary') if isinstance(primary_strict_contract, dict) @@ -1697,6 +1729,8 @@ def _build_ai_prompt_pack(report): 'real_case_calibration_layer': primary_real_case_calibration_layer or {}, 'technical_debt_contract': primary_technical_debt_contract or {}, 'remaining_priority1_batch_queue': primary_remaining_priority1_batch_queue or {}, + 'oracle_parity_queue': primary_oracle_parity_queue or {}, + 'release_hygiene_plan': primary_release_hygiene_plan or {}, 'strict_workflow_primary_route': strict_workflow_primary_route, 'strict_workflow_routes_available': list(strict_workflow_contracts.keys()), 'strict_workflow_contracts': strict_workflow_contracts, diff --git a/tests/test_fortune_output_pipeline_p0_p8.py b/tests/test_fortune_output_pipeline_p0_p8.py new file mode 100644 index 00000000..294a0da2 --- /dev/null +++ b/tests/test_fortune_output_pipeline_p0_p8.py @@ -0,0 +1,204 @@ +#!/usr/bin/env python3 +"""Golden-output and executable-queue contracts for fortune workflows.""" + +from __future__ import annotations + +import json +import subprocess +import sys +from pathlib import Path + +from mcp_server import _collect_strict_evidence + + +ROOT = Path(__file__).resolve().parents[1] +REQUIRED_OUTPUT_SECTIONS = [ + "promise", + "activation", + "manifestation", + "label", + "confidence_boundary", +] + + +def _run_engine(*args: str) -> dict: + completed = subprocess.run( + [sys.executable, "scripts/jyotish_engine.py", *args], + cwd=ROOT, + check=False, + text=True, + capture_output=True, + timeout=180, + ) + assert completed.returncode == 0, completed.stderr[-2000:] or completed.stdout[-2000:] + return json.loads(completed.stdout) + + +def _base_modules() -> dict: + return { + "modules": { + "chart": { + "ascendant": {"sign": "Leo"}, + "planets": { + "Sun": {"status": "中性(Neutral)"}, + "Moon": {"status": "中性(Neutral)"}, + "Mercury": {"status": "中性(Neutral)"}, + "Venus": {"status": "中性(Neutral)"}, + "Jupiter": {"status": "中性(Neutral)"}, + "Saturn": {"status": "中性(Neutral)"}, + }, + }, + "varga_full": { + "D10_Dasamsa": {"summary": "career varga present"}, + "D9_Navamsha": {"summary": "relationship varga present"}, + "D2_Hora": {"summary": "wealth varga present"}, + "D11_Rudramsha": {"summary": "gains varga present"}, + }, + "special_lagnas": { + "A10_Karma_Pada": {"sign": "Capricorn", "lord": "Saturn"}, + "Upapada_Lagna": {"sign": "Libra", "lord": "Venus"}, + }, + "jaimini": { + "karakas": { + "Amatyakaraka": {"planet": "Mercury"}, + "Atmakaraka": {"planet": "Sun"}, + "Darakaraka": {"planet": "Venus"}, + }, + "karakamsha": {"karakamsha_sign": "Leo", "karakamsha_lord": "Sun"}, + }, + "dasha": {"current_dasha": {"mahadasha": "Mercury", "antardasha": "Sun"}}, + "narayana_dasha": { + "current_dasha": { + "md": {"sign": "Capricorn", "lord": "Saturn"}, + "ad": {"sign": "Aquarius", "lord": "Saturn"}, + "pd": {"sign": "Pisces", "lord": "Jupiter"}, + } + }, + "dasa_convergence": { + "domain_activations": { + "career_status": {"convergence_level": "L2", "probability": "35-50%"}, + "marriage_relationship": {"convergence_level": "L2", "probability": "35-50%"}, + "wealth_income": {"convergence_level": "L2", "probability": "35-50%"}, + } + }, + } + } + + +def test_p0_chinese_golden_narratives_expose_required_contract_sections() -> None: + result = _run_engine( + "full-reading", + "--year", + "REDACTED_YEAR", + "--month", + "4", + "--day", + "17", + "--hour", + "14", + "--minute", + "49", + "--lat", + "36.466667", + "--lon", + "114.2", + "--tz", + "8", + "--today", + "2026-07-02", + "--transit-date", + "2026-07-02", + ) + snapshot = result["ai_prompt_pack"]["evidence_snapshot"] + + for key in ["career_narrative", "finance_narrative", "relationship_narrative"]: + narrative = snapshot[key] + markdown = narrative["markdown"] + assert narrative["output_template_status"] == "used" + assert narrative["required_sections"] == REQUIRED_OUTPUT_SECTIONS + for section in REQUIRED_OUTPUT_SECTIONS: + assert f"{section}:" in markdown + assert "MEVG" in markdown + assert "Real Case Calibration" in markdown + + for route in ["career", "relationship", "finance"]: + contract = snapshot["strict_workflow_contracts"][route] + assert contract["output_template_contract"]["required_sections"] == REQUIRED_OUTPUT_SECTIONS + + +def test_p1_p2_mevg_queue_and_real_case_index_are_executable_contracts() -> None: + strict = _collect_strict_evidence("career", _base_modules()) + + queue = strict["mevg_collection_queue"] + assert queue["status"] in {"queued", "blocked"} + assert queue["execution_mode"] == "cache_ttl_free_tier_queue" + assert queue["cache_ttl_hours"] >= 24 + assert queue["evidence_packet"]["packet_type"] == "mevg_external_evidence_packet" + assert queue["failure_record"]["status"] == "blocked_until_external_fetch" + assert queue["failure_record"]["blocked_reason"] + + case_layer = strict["real_case_calibration_layer"] + assert case_layer["batch_id"] == "real_case_studies_batch1" + assert case_layer["index_status"] == "available" + assert case_layer["fallback_policy"] == "downgrade_without_matching_cases" + assert "relationship" in case_layer["case_index_by_domain"] + assert "docs/benchmark/legacy-marriage-v6.1/verify-results-v6.1.json" in case_layer["case_index_by_domain"]["relationship"] + + +def test_p3_p4_domain_layers_affect_judgement_and_technical_debt_is_split() -> None: + strict = _collect_strict_evidence("career", _base_modules()) + judgement = strict["event_judgement"] + + for marker in [ + "dasha_timing_layer_used", + "varga_strength_layer_used", + "annual_special_layer_context", + "modifier_obstacle_layer_used", + ]: + assert marker in judgement["secondary_context"] + + debt = strict["technical_debt_contract"] + assert debt["narayana"]["status_breakdown"]["closed"] + assert debt["narayana"]["status_breakdown"]["blocked"] + assert debt["tajika"]["status_breakdown"]["closed"] + assert debt["tajika"]["status_breakdown"]["blocked"] + assert debt["oracle_parity"]["status"] == "blocked" + + +def test_p5_p6_p7_p8_frontend_batches_oracle_and_hygiene_are_visible() -> None: + result = _run_engine( + "full-reading", + "--year", + "REDACTED_YEAR", + "--month", + "4", + "--day", + "17", + "--hour", + "14", + "--minute", + "49", + "--lat", + "36.466667", + "--lon", + "114.2", + "--tz", + "8", + "--today", + "2026-07-02", + "--transit-date", + "2026-07-02", + ) + snapshot = result["ai_prompt_pack"]["evidence_snapshot"] + + assert snapshot["remaining_priority1_batch_queue"]["batch_statuses"]["real_case_studies_batch1"] == "next" + assert snapshot["oracle_parity_queue"]["systems"] == ["VedAstro", "PyJHora", "jyotishganit"] + assert snapshot["oracle_parity_queue"]["priority_domains"] == ["Dasha", "Shadbala", "Tajika", "Narayana"] + assert snapshot["release_hygiene_plan"]["git_sync_required"] is True + assert snapshot["release_hygiene_plan"]["gc_log_policy"] == "separate_safe_cleanup_plan_required" + + main_js = (ROOT / "jyotish-app" / "main.js").read_text(encoding="utf-8") + assert "core source refs" in main_js + assert "why confidence downgraded" in main_js + assert "next batch queue" in main_js + assert "oracle parity" in main_js