Surface VedAstro overview across prompts and reports

This commit is contained in:
732642856
2026-06-29 09:54:26 +08:00
parent 3161de68c0
commit 8a54dfbebd
7 changed files with 755 additions and 0 deletions
+74
View File
@@ -175,6 +175,16 @@ export function exportJSON(chartData, extras = {}) {
payload.modules.workflows = extras.workflows;
}
// ── 17. Vimsopaka Semantic Summary ──
if (extras.vimsopaka_semantic_summary) {
payload.modules = payload.modules || {};
payload.modules.vimsopaka_semantic_summary = extras.vimsopaka_semantic_summary;
}
if (extras.vedastro_overview) {
payload.modules = payload.modules || {};
payload.modules.vedastro_overview = extras.vedastro_overview;
}
downloadFile(
JSON.stringify(payload, null, 2),
`jyotish-chart-${birth_info?.date || 'export'}.json`,
@@ -395,6 +405,10 @@ function _buildHTMLReport(chartData, extras) {
const audit = extras.audit || {};
const workflows = extras.workflows || chartData._client_workflows || {};
const relationshipNarrative = extras.relationship_narrative || chartData?.ai_prompt_pack?.evidence_snapshot?.relationship_narrative || chartData?.relationship_narrative || null;
const vimsopakaSemanticSummary = extras.vimsopaka_semantic_summary || chartData?.ai_prompt_pack?.evidence_snapshot?.vimsopaka_semantic_summary || chartData?.vimsopaka_semantic_summary || null;
const functionalBeneficMalefic = extras.functional_benefic_malefic || chartData?.ai_prompt_pack?.evidence_snapshot?.functional_benefic_malefic || chartData?.functional_benefic_malefic || null;
const vedastroOverview = extras.vedastro_overview || chartData?.ai_prompt_pack?.evidence_snapshot?.vedastro_overview || chartData?.vedastro_overview || null;
const techniqueAuditTable = extras.technique_audit_table || chartData?.ai_prompt_pack?.evidence_snapshot?.technique_audit_table || chartData?.technique_audit_table || null;
const generatedAt = new Date().toLocaleString('zh-CN');
return `<!doctype html>
@@ -589,6 +603,12 @@ function _buildHTMLReport(chartData, extras) {
</table>` : '<p>当前导出未附带 Dasha 时间线。</p>'}
</section>
${_functionalRoleSummarySection(functionalBeneficMalefic)}
${_vimsopakaSemanticSummarySection(vimsopakaSemanticSummary)}
${_techniqueAuditTableSection(techniqueAuditTable)}
${_relationshipStrictNarrativeSection(relationshipNarrative)}
${_workflowReportSection(workflows)}
@@ -618,6 +638,60 @@ function _buildHTMLReport(chartData, extras) {
</html>`;
}
function _functionalRoleSummarySection(snapshot) {
if (!snapshot || typeof snapshot !== 'object') return '';
if (snapshot.status === 'blocked' || snapshot.status === 'not_used') return '';
const benefics = Array.isArray(snapshot.functional_benefics) ? snapshot.functional_benefics : [];
const malefics = Array.isArray(snapshot.functional_malefics) ? snapshot.functional_malefics : [];
const neutrals = Array.isArray(snapshot.functional_neutrals) ? snapshot.functional_neutrals : [];
const yogakarakas = Array.isArray(snapshot.yogakarakas) ? snapshot.yogakarakas : [];
return `<section class="functional-role-summary">
<h2>Functional Benefic/Malefic</h2>
<div class="note relationship-deliverable">
${_table([
['状态', snapshot.status || 'used'],
['Ascendant', snapshot.ascendant || snapshot.asc_sign || '-'],
['Functional Benefics', benefics.join('、') || '—'],
['Functional Malefics', malefics.join('、') || '—'],
['Functional Neutrals', neutrals.join('、') || '—'],
['Yogakarakas', yogakarakas.join('、') || '—'],
['Confidence Impact', snapshot.effect_on_confidence || '已进入高严谨裁决链。'],
])}
</div>
</section>`;
}
function _vimsopakaSemanticSummarySection(summary) {
if (!summary || typeof summary !== 'object') return '';
const highlights = Array.isArray(summary.highlights) ? summary.highlights : [];
const warnings = Array.isArray(summary.warnings) ? summary.warnings : [];
const status = summary.status || 'blocked';
return `<section class="vimsopaka-semantic-summary">
<h2>Vimsopaka 语义摘要</h2>
<div class="note relationship-deliverable">
<p>将 Vimsopaka dignity 结果转成用户可见语义,避免只剩分数没有解释边界。</p>
${_table([
['状态', status],
['积极语义', highlights.length ? highlights.join(';') : '—'],
['风险语义', warnings.length ? warnings.join(';') : '—'],
])}
</div>
</section>`;
}
function _techniqueAuditTableSection(rows) {
if (!Array.isArray(rows) || !rows.length) return '';
const formattedRows = rows.map(row => ([
row.technique || '-',
row.status || '-',
row.note || '-',
]));
return `<section class="technique-audit-table">
<h2>Technique Audit Table</h2>
${_table([['Technique', 'Status', 'Note'], ...formattedRows])}
</section>`;
}
function _relationshipStrictNarrativeSection(narrative) {
if (!narrative || typeof narrative !== 'object') return '';
const strengths = Array.isArray(narrative.strengths) ? narrative.strengths : [];
+16
View File
@@ -7481,6 +7481,18 @@ function buildExportExtras(sourceChart) {
|| sourceChart?.relationship_narrative
|| sourceChart?.modules?.relationship_strict_evidence?.user_narrative
|| null;
const vimsopakaSemanticSummary = sourceChart?.ai_prompt_pack?.evidence_snapshot?.vimsopaka_semantic_summary
|| sourceChart?.vimsopaka_semantic_summary
|| null;
const functionalBeneficMalefic = sourceChart?.ai_prompt_pack?.evidence_snapshot?.functional_benefic_malefic
|| sourceChart?.functional_benefic_malefic
|| null;
const vedastroOverview = sourceChart?.ai_prompt_pack?.evidence_snapshot?.vedastro_overview
|| sourceChart?.vedastro_overview
|| null;
const techniqueAuditTable = sourceChart?.ai_prompt_pack?.evidence_snapshot?.technique_audit_table
|| sourceChart?.technique_audit_table
|| null;
return {
dasha, yogas, vargas, aspects, jaimini,
@@ -7488,6 +7500,10 @@ function buildExportExtras(sourceChart) {
shadbala: sb, ashtakavarga: _buildAVModule(av),
panchanga, validation, audit, actionableContext,
relationship_narrative: relationshipNarrative,
vimsopaka_semantic_summary: vimsopakaSemanticSummary,
functional_benefic_malefic: functionalBeneficMalefic,
vedastro_overview: vedastroOverview,
technique_audit_table: techniqueAuditTable,
provenance: sourceChart._client_audit?.provenance,
workflows: buildWorkflowExportExtras(sourceChart),
};
+293
View File
@@ -14,13 +14,20 @@ import io
import json, sys, os, math
import importlib.util
import re
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime, timedelta
from http.server import HTTPServer, BaseHTTPRequestHandler
from urllib.parse import urlparse
try:
from scripts.local_env import load_local_env
except ModuleNotFoundError: # pragma: no cover - script execution path
from local_env import load_local_env
SCRIPTS_DIR = os.path.dirname(os.path.abspath(__file__))
REPO_ROOT = os.path.abspath(os.path.join(SCRIPTS_DIR, '..'))
sys.path.insert(0, SCRIPTS_DIR)
load_local_env(REPO_ROOT)
_LOCAL_MODULE_CACHE = {}
@@ -38,6 +45,158 @@ def _load_local_module(module_name):
_LOCAL_MODULE_CACHE[module_name] = module
return module
def _attach_vedastro_main_entry_overview(chart_result, birth_payload):
if not isinstance(chart_result, dict):
return chart_result
modules = chart_result.setdefault('modules', {})
if not isinstance(modules, dict):
modules = {}
chart_result['modules'] = modules
if modules.get('vedastro_range_scan_result'):
return chart_result
try:
adapter = _load_local_module('vedastro_service_adapter')
except Exception:
return chart_result
reference_date = str(
birth_payload.get('transit_date')
or birth_payload.get('today')
or datetime.utcnow().strftime('%Y-%m-%d')
)[:10]
start_date = datetime.strptime(reference_date, '%Y-%m-%d').date()
end_date = start_date
case = {
'year': birth_payload.get('year'),
'month': birth_payload.get('month'),
'day': birth_payload.get('day'),
'hour': birth_payload.get('hour'),
'minute': birth_payload.get('minute'),
'second': birth_payload.get('second', 0),
'lat': birth_payload.get('lat'),
'lon': birth_payload.get('lon'),
'tz': birth_payload.get('tz'),
'ayanamsa_policy': birth_payload.get('ayanamsa') or 'lahiri',
'node_policy': birth_payload.get('node_mode') or birth_payload.get('nodeMode') or 'mean',
}
def _scan_domain(domain: str):
return domain, adapter.run_range_scan_for_case(
case,
domain=domain,
start_date=start_date.isoformat(),
end_date=end_date.isoformat(),
case_id=f"api_chart_{domain}",
)
domain_reports = {}
combined_events = []
domain_statuses = {}
top_events = {}
failure_reason = None
availability = True
with ThreadPoolExecutor(max_workers=3) as executor:
for domain, domain_report in executor.map(_scan_domain, ('career', 'marriage', 'wealth')):
domain_reports[domain] = domain_report
for domain in ('career', 'marriage', 'wealth'):
domain_report = domain_reports[domain]
domain_statuses[domain] = domain_report.get('status')
availability = availability and bool(domain_report.get('available', False))
if domain_report.get('status') != 'ok' and failure_reason is None:
failure_reason = domain_report.get('reason')
for event in domain_report.get('evidence_ledger') or []:
if isinstance(event, dict):
combined_events.append(event)
top_event = domain_report.get('top_event')
if isinstance(top_event, dict):
top_events[domain] = top_event
primary_status = next(
(
domain_reports[domain].get('status')
for domain in ('career', 'marriage', 'wealth')
if domain_reports.get(domain, {}).get('status') == 'ok'
),
domain_reports.get('marriage', {}).get('status') or 'blocked',
)
source_metadata = {
'ingestion_profile': 'main_entry_overview',
'search_scope': 'single_day_overview',
'reference_date': reference_date,
'scan_window': {'start': start_date.isoformat(), 'end': end_date.isoformat()},
'domain_statuses': domain_statuses,
'domain_event_counts': {
domain: int((domain_reports.get(domain, {}) or {}).get('event_count', 0) or 0)
for domain in ('career', 'marriage', 'wealth')
},
}
for domain in ('career', 'marriage', 'wealth'):
metadata = domain_reports.get(domain, {}).get('source_metadata')
if isinstance(metadata, dict):
for key in (
'endpoint',
'endpoint_host',
'transport',
'provenance_mode',
'timeout_seconds',
'retry_policy',
):
if key in metadata and key not in source_metadata:
source_metadata[key] = metadata[key]
modules['vedastro_range_scan_result'] = {
'backend': 'vedastro_service_adapter_candidate',
'available': availability,
'status': primary_status,
'operation': 'range_scan',
'domain': 'overview',
'event_count': len(combined_events),
'top_event': top_events.get('marriage') or next(iter(top_events.values()), None),
'top_events_by_domain': top_events,
'evidence_ledger': combined_events,
'source_metadata': source_metadata,
'reason': failure_reason,
'domain_reports': domain_reports,
}
return chart_result
def _build_vedastro_overview_payload_from_chart(chart):
modules = chart.get('modules') if isinstance(chart, dict) else {}
overview = modules.get('vedastro_range_scan_result') if isinstance(modules, dict) else {}
if not isinstance(overview, dict):
return {
'status': 'blocked',
'source': 'vedastro_service_adapter_candidate',
'ingestion_profile': None,
'search_scope': None,
'reference_date': None,
'event_count': 0,
'domain_statuses': {},
'top_events_by_domain': {},
'boundary_note': 'VedAstro main-entry overview was not attached.',
'visibility': 'user_visible_overview_only',
}
metadata = overview.get('source_metadata') if isinstance(overview.get('source_metadata'), dict) else {}
return {
'status': overview.get('status') or 'blocked',
'source': overview.get('backend') or 'vedastro_service_adapter_candidate',
'ingestion_profile': metadata.get('ingestion_profile'),
'search_scope': metadata.get('search_scope'),
'reference_date': metadata.get('reference_date'),
'event_count': int(overview.get('event_count', 0) or 0),
'domain_statuses': metadata.get('domain_statuses') or {},
'top_events_by_domain': overview.get('top_events_by_domain') or {},
'boundary_note': (
overview.get('reason')
or 'This is overview only and does not replace explicit long-range VedAstro scans.'
),
'visibility': 'user_visible_overview_only',
}
SIGNS = ['Aries','Taurus','Gemini','Cancer','Leo','Virgo',
'Libra','Scorpio','Sagittarius','Capricorn','Aquarius','Pisces']
@@ -533,6 +692,39 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
return html[:body_close.start()] + summary + html[body_close.start():]
return html + summary
def _inject_vimsopaka_semantic_summary(self, html, snapshot):
if not isinstance(snapshot, dict):
return html
if snapshot.get('status') in {None, 'blocked'}:
return html
highlights = snapshot.get('highlights') if isinstance(snapshot.get('highlights'), list) else []
warnings = snapshot.get('warnings') if isinstance(snapshot.get('warnings'), list) else []
if not highlights and not warnings:
return html
def _escape(value):
return html_lib.escape(str(value or ''))
def _line_items(items):
return ''.join(f'<li>{_escape(item)}</li>' for item in items) or '<li>None</li>'
summary = (
'<section data-vimsopaka-semantic-summary="true" '
'style="margin:24px 0;padding:16px;border:1px solid #d9dde8;border-radius:8px;'
'background:#f7f9fc;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;">'
'<h2 style="margin:0 0 12px;font-size:20px;">Vimsopaka Semantic Summary</h2>'
f'<p style="margin:0 0 8px;"><strong>Status:</strong> {_escape(snapshot.get("status"))}</p>'
'<p style="margin:0 0 8px;"><strong>Highlights:</strong></p>'
f'<ul style="margin:0 0 8px 18px;padding:0;">{_line_items(highlights)}</ul>'
'<p style="margin:0 0 8px;"><strong>Warnings:</strong></p>'
f'<ul style="margin:0 0 8px 18px;padding:0;">{_line_items(warnings)}</ul>'
'</section>'
)
body_close = re.search(r'</body\s*>', html, re.IGNORECASE)
if body_close:
return html[:body_close.start()] + summary + html[body_close.start():]
return html + summary
def _inject_relationship_narrative_summary(self, html, narrative):
if not isinstance(narrative, dict):
return html
@@ -592,6 +784,49 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
return html[:body_close.start()] + summary + html[body_close.start():]
return html + summary
def _inject_vedastro_overview_summary(self, html, overview):
if not isinstance(overview, dict):
return html
if not overview.get('status'):
return html
def _escape(value):
return html_lib.escape(str(value or ''))
domain_statuses = overview.get('domain_statuses') if isinstance(overview.get('domain_statuses'), dict) else {}
top_events = overview.get('top_events_by_domain') if isinstance(overview.get('top_events_by_domain'), dict) else {}
domain_lines = ''.join(
f'<li><strong>{_escape(domain)}:</strong> {_escape(status)}</li>'
for domain, status in domain_statuses.items()
) or '<li>None</li>'
top_event_lines = ''.join(
f'<li><strong>{_escape(domain)}:</strong> {_escape((payload or {}).get("signal_label") or (payload or {}).get("event_id") or "-")} · {_escape((payload or {}).get("start") or "-")}</li>'
for domain, payload in top_events.items()
if isinstance(payload, dict)
) or '<li>None</li>'
summary = (
'<section data-vedastro-overview-summary="true" '
'style="margin:24px 0;padding:16px;border:1px solid #d9dde8;border-radius:8px;'
'background:#f7f9fc;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;">'
'<h2 style="margin:0 0 12px;font-size:20px;">VedAstro External Overview</h2>'
f'<p style="margin:0 0 8px;"><strong>Status:</strong> {_escape(overview.get("status"))}</p>'
f'<p style="margin:0 0 8px;"><strong>Ingestion Profile:</strong> {_escape(overview.get("ingestion_profile"))}</p>'
f'<p style="margin:0 0 8px;"><strong>Search Scope:</strong> {_escape(overview.get("search_scope"))}</p>'
f'<p style="margin:0 0 8px;"><strong>Reference Date:</strong> {_escape(overview.get("reference_date"))}</p>'
f'<p style="margin:0 0 8px;"><strong>Event Count:</strong> {_escape(overview.get("event_count"))}</p>'
'<p style="margin:0 0 8px;"><strong>Domain Statuses:</strong></p>'
f'<ul style="margin:0 0 8px 18px;padding:0;">{domain_lines}</ul>'
'<p style="margin:0 0 8px;"><strong>Top Events By Domain:</strong></p>'
f'<ul style="margin:0 0 8px 18px;padding:0;">{top_event_lines}</ul>'
f'<p style="margin:0;color:#5b6472;font-size:13px;"><strong>Boundary:</strong> {_escape(overview.get("boundary_note"))}</p>'
'</section>'
)
body_close = re.search(r'</body\s*>', html, re.IGNORECASE)
if body_close:
return html[:body_close.start()] + summary + html[body_close.start():]
return html + summary
def _artifact_base64(self, path):
size = os.path.getsize(path)
if size > MAX_REPORT_BASE64_BYTES:
@@ -727,10 +962,18 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
html,
body.get('functional_benefic_malefic'),
)
html = self._inject_vimsopaka_semantic_summary(
html,
body.get('vimsopaka_semantic_summary'),
)
html = self._inject_relationship_narrative_summary(
html,
body.get('relationship_narrative'),
)
html = self._inject_vedastro_overview_summary(
html,
body.get('vedastro_overview'),
)
fmt = body.get('format', 'html')
if fmt not in {'html', 'pdf'}:
raise BadRequest('format must be html or pdf')
@@ -1999,6 +2242,31 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
'available_dashas': dasha_list,
'dasha_count': len(dasha_list),
}
result['modules'] = {
'chart': {
'planets': result['planets'],
'ascendant': result['ascendant'],
'houses': result['houses'],
'birth_info': result['birth'],
},
'dasha': result['dasha'],
'shadbala': {'planets': sb.get('planets', {})} if 'sb' in locals() and isinstance(sb, dict) else {},
}
_attach_vedastro_main_entry_overview(result, {
'year': year,
'month': month,
'day': day,
'hour': int(hour),
'minute': int(minute),
'second': int(second),
'lat': lat,
'lon': lon,
'tz': tz,
'ayanamsa': ayanamsa_name,
'node_mode': body.get('node_mode', body.get('nodeMode', 'mean')),
'today': body.get('today') or body.get('current_date'),
'transit_date': body.get('transit_date'),
})
result['ai_prompt_pack'] = self._build_chart_prompt_pack(result)
return result
except ImportError:
@@ -2059,6 +2327,29 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
'special_lagnas': special_lagnas,
'available_dashas': [], 'dasha_count': 0,
}
result['modules'] = {
'chart': {
'planets': result['planets'],
'ascendant': result['ascendant'],
'houses': result['houses'],
'birth_info': result['birth'],
},
'dasha': result['dasha'],
'shadbala': {},
}
_attach_vedastro_main_entry_overview(result, {
'year': year,
'month': month,
'day': day,
'hour': int(hour),
'minute': int(minute),
'second': int(second),
'lat': lat,
'lon': lon,
'tz': tz,
'ayanamsa': 'lahiri',
'node_mode': 'mean',
})
result['ai_prompt_pack'] = self._build_chart_prompt_pack(result)
return result
@@ -2069,6 +2360,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
dasha = chart.get('dasha') or {}
shadbala = chart.get('shadbala') or {}
functional_layer = self._functional_benefic_malefic_snapshot(planets, ascendant)
vedastro_overview = _build_vedastro_overview_payload_from_chart(chart)
top_strength = sorted(
[
{
@@ -2138,6 +2430,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
'shadbala_ranking': top_strength,
},
'functional_benefic_malefic': functional_layer,
'vedastro_overview': vedastro_overview,
'quality_boundary': {
'external_oracle_status': 'D1/D9/VedAstro longitude boundary covered; Dasha/Shadbala external absolute calibration still requires multi-source oracle expansion.',
},
+213
View File
@@ -43,6 +43,7 @@ import csv
import math
import sqlite3
import importlib.util
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime, timedelta
from typing import Dict, List
from tabulate import tabulate
@@ -67,6 +68,13 @@ DB_PATH = os.path.join(CLAW_DIR, 'vedic_astrology_validation.db')
PERSON_CSV = os.path.join(CLAW_DIR, 'vedastro_data', 'PersonList-15k.csv')
TRANSIT_JSON = os.path.join(CLAW_DIR, '月运过境配置-2026-2028.json')
try:
from local_env import load_local_env
except ModuleNotFoundError: # pragma: no cover - compatibility when imported as package
from scripts.local_env import load_local_env
load_local_env(ROOT_DIR)
try:
import swisseph as swe
apply_ayanamsa('lahiri', swe) # P0修复:必须设置Lahiri恒星黄道模式
@@ -140,6 +148,29 @@ DIGNITY_LABELS = {
'DEBILITATED': '落陷(Debilitated)',
'NEECHA_BHANGA': '落陷取消(Neecha Bhanga)',
}
def _build_vimsopaka_semantic_summary(vimsopaka: dict | None) -> dict:
if not isinstance(vimsopaka, dict):
return {"status": "blocked", "highlights": [], "warnings": []}
highlights = []
warnings = []
for planet, payload in vimsopaka.items():
if not isinstance(payload, dict):
continue
dignity = payload.get('dignity')
label = DIGNITY_LABELS.get(dignity)
if dignity in {'GREAT_FRIEND', 'NEECHA_BHANGA'} and label:
highlights.append(f"{planet}: {label}")
elif dignity == 'GREAT_ENEMY' and label:
warnings.append(f"{planet}: {label}")
return {
"status": "used",
"highlights": highlights,
"warnings": warnings,
}
PUSHKARA_NAVAMSA_RANGES = {
'fire': [(6 + 40/60, 10), (23 + 20/60, 26 + 40/60)],
'earth': [(3 + 20/60, 6 + 40/60), (16 + 40/60, 20)],
@@ -888,6 +919,7 @@ def _build_technique_audit_table(functional_layer, oracle_progress, modules):
vimsopaka = modules.get('vimsopaka') if isinstance(modules, dict) else {}
dasa_convergence = modules.get('dasa_convergence') if isinstance(modules, dict) else {}
relationship = modules.get('relationship_strict_evidence') if isinstance(modules, dict) else {}
vedastro_overview = modules.get('vedastro_range_scan_result') if isinstance(modules, dict) else {}
rows = [
{
@@ -959,6 +991,17 @@ def _build_technique_audit_table(functional_layer, oracle_progress, modules):
"不得把这些支持越权解释成 legal marriage 的高置信度落地。"
),
})
vedastro_meta = vedastro_overview.get('source_metadata') if isinstance(vedastro_overview, dict) else {}
rows.append({
'technique': 'VedAstro Main Entry Overview',
'status': 'used' if isinstance(vedastro_overview, dict) and vedastro_overview.get('status') == 'ok' else 'blocked',
'source': 'modules.vedastro_range_scan_result',
'note': (
f"overview only; status={vedastro_overview.get('status') if isinstance(vedastro_overview, dict) else None}; "
f"domain_statuses={vedastro_meta.get('domain_statuses') if isinstance(vedastro_meta, dict) else None}; "
f"reference_date={vedastro_meta.get('reference_date') if isinstance(vedastro_meta, dict) else None}."
),
})
return rows
@@ -1068,6 +1111,39 @@ def _build_relationship_narrative_payload(relationship_strict):
}
def _build_vedastro_overview_payload(modules):
overview = modules.get('vedastro_range_scan_result') if isinstance(modules, dict) else {}
if not isinstance(overview, dict):
return {
'status': 'blocked',
'source': 'vedastro_service_adapter_candidate',
'ingestion_profile': None,
'search_scope': None,
'reference_date': None,
'event_count': 0,
'domain_statuses': {},
'top_events_by_domain': {},
'boundary_note': 'VedAstro main-entry overview was not attached.',
'visibility': 'user_visible_overview_only',
}
metadata = overview.get('source_metadata') if isinstance(overview.get('source_metadata'), dict) else {}
return {
'status': overview.get('status') or 'blocked',
'source': overview.get('backend') or 'vedastro_service_adapter_candidate',
'ingestion_profile': metadata.get('ingestion_profile'),
'search_scope': metadata.get('search_scope'),
'reference_date': metadata.get('reference_date'),
'event_count': int(overview.get('event_count', 0) or 0),
'domain_statuses': metadata.get('domain_statuses') or {},
'top_events_by_domain': overview.get('top_events_by_domain') or {},
'boundary_note': (
overview.get('reason')
or 'This is overview only and does not replace explicit long-range VedAstro scans.'
),
'visibility': 'user_visible_overview_only',
}
def _build_ai_prompt_pack(report):
"""Build a compact, evidence-first prompt pack for downstream AI/RAG reading."""
modules = report.get('modules', {}) if isinstance(report, dict) else {}
@@ -1087,6 +1163,8 @@ def _build_ai_prompt_pack(report):
oracle_progress = _oracle_progress_snapshot()
technique_audit_table = _build_technique_audit_table(functional_layer, oracle_progress, modules)
relationship_narrative = _build_relationship_narrative_payload(modules.get('relationship_strict_evidence'))
vimsopaka_semantic_summary = _build_vimsopaka_semantic_summary(modules.get('vimsopaka'))
vedastro_overview = _build_vedastro_overview_payload(modules)
shadbala_ranking = []
for planet_name, pdata in sorted(
@@ -1157,8 +1235,10 @@ def _build_ai_prompt_pack(report):
},
'oracle_progress': oracle_progress,
'functional_benefic_malefic': functional_layer,
'vedastro_overview': vedastro_overview,
'technique_audit_table': technique_audit_table,
'relationship_narrative': relationship_narrative,
'vimsopaka_semantic_summary': vimsopaka_semantic_summary,
}
prompt_lines = [
@@ -1169,6 +1249,7 @@ def _build_ai_prompt_pack(report):
"必须显式标注置信度和边界:Dasha/PDF 起点差异、Shadbala 外部绝对值 oracle 尚未完成时,不得声称已经完全校准。",
"输出结构建议:参数声明、核心星盘、关系/事业/财富/健康分主题、当前时机、证据表、风险边界、可行动建议。",
"若引用经典法则,请优先检索 retrieval_plan.local_reference_docs;需要外部断语时再做 web/source verification。",
"若 evidence_snapshot.vedastro_overview.status 为 ok,请把它作为用户可见外部概览证据明确写出,但不要把 overview-only 结果误当作长周期精扫结论。",
]
return {
@@ -1196,6 +1277,133 @@ def _build_ai_prompt_pack(report):
}
def _overview_vedastro_reference_date(args) -> str:
candidate = (
getattr(args, 'transit_date', None)
or getattr(args, 'today', None)
or datetime.now().strftime('%Y-%m-%d')
)
return str(candidate)[:10]
def _overview_vedastro_scan_window(reference_date: str) -> tuple[str, str]:
start = datetime.strptime(reference_date, '%Y-%m-%d').date()
return start.isoformat(), start.isoformat()
def _attach_vedastro_main_entry_overview(report, args):
if not isinstance(report, dict):
return report
modules = report.setdefault('modules', {})
if not isinstance(modules, dict):
return report
if modules.get('vedastro_range_scan_result'):
return report
try:
from vedastro_service_adapter import run_range_scan_for_case
except Exception as exc: # pragma: no cover - import guard
report.setdefault('warnings', []).append(f"vedastro-main-entry-import: {exc}")
return report
reference_date = _overview_vedastro_reference_date(args)
start_date, end_date = _overview_vedastro_scan_window(reference_date)
case = {
'year': getattr(args, 'year', None),
'month': getattr(args, 'month', None),
'day': getattr(args, 'day', None),
'hour': getattr(args, 'hour', None),
'minute': getattr(args, 'minute', None),
'second': _arg_second(args),
'lat': getattr(args, 'lat', None),
'lon': getattr(args, 'lon', None),
'tz': getattr(args, 'tz', None),
'ayanamsa_policy': getattr(args, 'ayanamsa', None) or _current_ayanamsa_name(args),
'node_policy': getattr(args, 'node_mode', 'mean'),
}
def _scan_domain(domain: str):
return domain, run_range_scan_for_case(
case,
domain=domain,
start_date=start_date,
end_date=end_date,
case_id=f"main_entry_{domain}",
)
domain_reports = {}
combined_events = []
domain_statuses = {}
top_events = {}
failure_reason = None
availability = True
with ThreadPoolExecutor(max_workers=3) as executor:
for domain, domain_report in executor.map(_scan_domain, ('career', 'marriage', 'wealth')):
domain_reports[domain] = domain_report
for domain in ('career', 'marriage', 'wealth'):
domain_report = domain_reports[domain]
domain_statuses[domain] = domain_report.get('status')
availability = availability and bool(domain_report.get('available', False))
if domain_report.get('status') != 'ok' and failure_reason is None:
failure_reason = domain_report.get('reason')
for event in domain_report.get('evidence_ledger') or []:
if isinstance(event, dict):
combined_events.append(event)
top_event = domain_report.get('top_event')
if isinstance(top_event, dict):
top_events[domain] = top_event
primary_status = next(
(
domain_reports[domain].get('status')
for domain in ('career', 'marriage', 'wealth')
if domain_reports.get(domain, {}).get('status') == 'ok'
),
domain_reports.get('marriage', {}).get('status') or 'blocked',
)
source_metadata = {
'ingestion_profile': 'main_entry_overview',
'search_scope': 'single_day_overview',
'reference_date': reference_date,
'scan_window': {'start': start_date, 'end': end_date},
'domain_statuses': domain_statuses,
'domain_event_counts': {
domain: int((domain_reports.get(domain, {}) or {}).get('event_count', 0) or 0)
for domain in ('career', 'marriage', 'wealth')
},
}
for domain in ('career', 'marriage', 'wealth'):
metadata = domain_reports.get(domain, {}).get('source_metadata')
if isinstance(metadata, dict):
for key in (
'endpoint',
'endpoint_host',
'transport',
'provenance_mode',
'timeout_seconds',
'retry_policy',
):
if key in metadata and key not in source_metadata:
source_metadata[key] = metadata[key]
modules['vedastro_range_scan_result'] = {
'backend': 'vedastro_service_adapter_candidate',
'available': availability,
'status': primary_status,
'operation': 'range_scan',
'domain': 'overview',
'event_count': len(combined_events),
'top_event': top_events.get('marriage') or next(iter(top_events.values()), None),
'top_events_by_domain': top_events,
'evidence_ledger': combined_events,
'source_metadata': source_metadata,
'reason': failure_reason,
'domain_reports': domain_reports,
}
return report
def _load_relationship_strict_collector():
try:
from mcp_server import _collect_strict_evidence as collector
@@ -4917,6 +5125,11 @@ def cmd_full_reading(args):
except Exception as e:
report['errors'].append(f"relationship-strict-evidence: {e}")
try:
_attach_vedastro_main_entry_overview(report, args)
except Exception as e:
report['warnings'].append(f"vedastro-main-entry-overview: {e}")
report['ai_prompt_pack'] = _build_ai_prompt_pack(report)
return report
+95
View File
@@ -190,6 +190,18 @@ def test_vedastro_status_endpoint_exposes_safe_adapter_state(monkeypatch) -> Non
assert payload['live_profile'] == 'vedastro-live'
def test_vedastro_status_endpoint_honors_explicit_network_disable_even_when_local_env_exists(monkeypatch) -> None:
monkeypatch.setenv('VEDASTRO_API_ENDPOINT', 'https://vedastro.example.test/secret/path')
monkeypatch.setenv('VEDASTRO_ENABLE_NETWORK', '0')
handler = _VedAstroStatusCaptureHandler()
handler.do_GET()
payload = handler.payload()
assert payload['network_enabled'] is False
assert payload['status'] == 'network_execution_disabled'
def test_vedastro_range_scan_endpoint_uses_user_birth_and_returns_controlled_blocked_state(monkeypatch) -> None:
monkeypatch.delenv('VEDASTRO_API_ENDPOINT', raising=False)
monkeypatch.delenv('VEDASTRO_ENABLE_NETWORK', raising=False)
@@ -578,6 +590,59 @@ def test_report_artifact_can_render_functional_benefic_malefic_summary() -> None
assert '高严谨模式下必须叠加功能性吉凶星。' in html
def test_report_artifact_can_render_vimsopaka_semantic_summary() -> None:
handler = _handler()
result = handler._compute_report_artifact({
'format': 'html',
'name': 'vimsopaka-semantic-report',
'html': '<!doctype html><html><body><h1>Jyotish</h1></body></html>',
'vimsopaka_semantic_summary': {
'status': 'used',
'highlights': ['Sun: 极友(Great Friend)', 'Moon: 落陷取消(Neecha Bhanga)'],
'warnings': ['Mars: 极敌(Great Enemy)'],
},
})
assert result['success'] is True
html = Path(result['html_path']).read_text(encoding='utf-8')
assert 'Vimsopaka Semantic Summary' in html
assert 'Great Friend' in html
assert 'Neecha Bhanga' in html
assert 'Great Enemy' in html
def test_report_artifact_can_render_vedastro_external_overview() -> None:
handler = _handler()
result = handler._compute_report_artifact({
'format': 'html',
'name': 'vedastro-overview-report',
'html': '<!doctype html><html><body><h1>Jyotish</h1></body></html>',
'vedastro_overview': {
'status': 'ok',
'source': 'vedastro_service_adapter_candidate',
'ingestion_profile': 'main_entry_overview',
'search_scope': 'single_day_overview',
'reference_date': '2026-06-29',
'event_count': 5,
'domain_statuses': {'career': 'ok', 'marriage': 'ok', 'wealth': 'ok'},
'top_events_by_domain': {
'marriage': {'signal_label': 'Jupiter in 7th marriage window', 'start': '2026-06-29'},
'career': {'signal_label': 'Jupiter in 10th career window', 'start': '2026-06-29'},
},
'boundary_note': 'This is overview only and does not replace explicit long-range scans.',
},
})
assert result['success'] is True
html = Path(result['html_path']).read_text(encoding='utf-8')
assert 'VedAstro External Overview' in html
assert 'main_entry_overview' in html
assert 'single_day_overview' in html
assert '2026-06-29' in html
assert 'Jupiter in 7th marriage window' in html
assert 'overview only' in html
def test_report_artifact_can_render_relationship_strict_narrative_summary() -> None:
handler = _handler()
result = handler._compute_report_artifact({
@@ -1534,6 +1599,36 @@ def test_chart_ai_prompt_pack_exposes_functional_benefic_malefic_layer() -> None
assert isinstance(functional['functional_benefics'], list)
assert isinstance(functional['functional_malefics'], list)
assert functional['effect_on_confidence']
vedastro = prompt_pack['evidence_snapshot']['vedastro_overview']
assert vedastro['source'] == 'vedastro_service_adapter_candidate'
assert vedastro['ingestion_profile'] == 'main_entry_overview'
assert vedastro['visibility'] == 'user_visible_overview_only'
def test_chart_auto_attaches_vedastro_main_entry_boundary(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("VEDASTRO_API_ENDPOINT", "https://example.invalid/api")
monkeypatch.setenv("VEDASTRO_ENABLE_NETWORK", "0")
monkeypatch.setenv("JYOTISH_SKIP_LOCAL_ENV", "1")
handler = _handler()
result = handler._compute_chart({
'year': 1990,
'month': 6,
'day': 15,
'hour': 12,
'minute': 0,
'lat': 39.9,
'lon': 116.4,
'tz': 8,
})
assert "modules" in result
vedastro = result["modules"]["vedastro_range_scan_result"]
assert vedastro["backend"] == "vedastro_service_adapter_candidate"
assert vedastro["status"] == "network_execution_disabled"
assert vedastro["source_metadata"]["ingestion_profile"] == "main_entry_overview"
assert vedastro["source_metadata"]["reference_date"]
assert sorted(vedastro["source_metadata"]["domain_statuses"]) == ["career", "marriage", "wealth"]
def test_yogas_endpoint_returns_summary_counts() -> None:
+47
View File
@@ -313,6 +313,10 @@ def test_full_reading_reports_ayanamsa_metadata_and_ai_prompt_pack() -> None:
assert relationship_narrative["markdown"]
assert "D9" in "".join(relationship_narrative["boundaries"])
assert "dual dasha" in relationship_narrative["markdown"]
vimsopaka_summary = prompt_pack["evidence_snapshot"]["vimsopaka_semantic_summary"]
assert vimsopaka_summary["status"] == "used"
assert isinstance(vimsopaka_summary["highlights"], list)
assert isinstance(vimsopaka_summary["warnings"], list)
oracle_progress = prompt_pack["evidence_snapshot"]["oracle_progress"]
assert oracle_progress["scope"] == "external_oracle_evidence_validation"
assert oracle_progress["valid_packets"] >= 4
@@ -320,6 +324,49 @@ def test_full_reading_reports_ayanamsa_metadata_and_ai_prompt_pack() -> None:
assert oracle_progress["production_tuning_allowed"] is False
assert oracle_progress["artifact_policy"] == "references/oracle/artifacts/"
assert "external_verified" in oracle_progress["promotion_rule"]
vedastro_overview = prompt_pack["evidence_snapshot"]["vedastro_overview"]
assert vedastro_overview["status"] in {"ok", "network_execution_disabled", "service_endpoint_not_configured"}
assert vedastro_overview["source"] == "vedastro_service_adapter_candidate"
assert vedastro_overview["ingestion_profile"] == "main_entry_overview"
assert vedastro_overview["visibility"] == "user_visible_overview_only"
vedastro_rows = [row for row in audit_table if row["technique"] == "VedAstro Main Entry Overview"]
assert vedastro_rows
assert vedastro_rows[0]["status"] in {"used", "blocked"}
assert "overview only" in vedastro_rows[0]["note"]
assert "domain_statuses" in vedastro_rows[0]["note"]
def test_full_reading_auto_attaches_vedastro_main_entry_boundary() -> None:
env = dict(**__import__("os").environ)
env["VEDASTRO_API_ENDPOINT"] = "https://example.invalid/api"
env["VEDASTRO_ENABLE_NETWORK"] = "0"
env["JYOTISH_SKIP_LOCAL_ENV"] = "1"
completed = subprocess.run(
[
sys.executable,
str(ENGINE),
"full-reading",
*BASE_BIRTH_ARGS,
"--today",
"2026-06-29",
"--transit-date",
"2026-06-29",
],
cwd=ROOT,
check=True,
capture_output=True,
text=True,
env=env,
)
result = json.loads(completed.stdout)
vedastro = result["modules"]["vedastro_range_scan_result"]
assert vedastro["backend"] == "vedastro_service_adapter_candidate"
assert vedastro["status"] == "network_execution_disabled"
assert vedastro["source_metadata"]["ingestion_profile"] == "main_entry_overview"
assert vedastro["source_metadata"]["reference_date"] == "2026-06-29"
assert sorted(vedastro["source_metadata"]["domain_statuses"]) == ["career", "marriage", "wealth"]
def test_full_reading_exposes_sensitive_point_modules() -> None:
+17
View File
@@ -1931,7 +1931,24 @@ def test_provenance_panchanga_workspace_panel_is_productized() -> None:
assert "_relationshipStrictNarrativeSection" in export_js
assert "relationship_report" in export_js
assert "relationship_narrative" in export_js
assert "vimsopaka_semantic_summary" in export_js
assert "functional_benefic_malefic" in export_js
assert "vedastro_overview" in export_js
assert "technique_audit_table" in export_js
assert "relationship_narrative" in main
assert "vimsopaka_semantic_summary" in main
assert "functional_benefic_malefic:" in main
assert "vedastro_overview:" in main
assert "technique_audit_table:" in main
assert "_functionalRoleSummarySection" in export_js
assert "_techniqueAuditTableSection" in export_js
assert "_vimsopakaSemanticSummarySection" in export_js
assert "Functional Benefic/Malefic" in export_js
assert "Technique Audit Table" in export_js
assert "Vimsopaka 语义摘要" in export_js
assert "highlights" in export_js
assert "warnings" in export_js
assert "vimsopaka_semantic_summary:" in main
assert "strictNarrative" in main
assert "relationship-deliverable" in export_js
assert "relationship-evidence-grid" in export_js