Land relationship narrative and dynamic hook workflow

This commit is contained in:
732642856
2026-06-29 03:54:45 +08:00
parent 25000647e7
commit 8466e3a71c
7 changed files with 946 additions and 13 deletions
+43
View File
@@ -394,6 +394,7 @@ function _buildHTMLReport(chartData, extras) {
const validation = extras.validation || {};
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 generatedAt = new Date().toLocaleString('zh-CN');
return `<!doctype html>
@@ -463,6 +464,12 @@ function _buildHTMLReport(chartData, extras) {
.relationship-list { background: #fff; border: 1px solid var(--line); border-radius: 6px; padding: 9px; break-inside: avoid; }
.relationship-list strong { color: var(--accent); }
.relationship-list ul { margin: 6px 0 0; padding-left: 18px; color: var(--muted); }
.relationship-caution {
background: #fff8ed; border: 1px solid #f3d19c; border-left: 4px solid #c67a00;
border-radius: 6px; padding: 10px 12px; margin: 10px 0 12px; break-inside: avoid;
}
.relationship-caution strong { display: block; color: #7a4b00; margin-bottom: 4px; }
.relationship-caution span { display: block; color: #7a4b00; font-size: 12px; }
.relationship-boundary { background: #fff8ed; border: 1px solid #ead7b8; border-radius: 6px; padding: 9px; margin-top: 8px; }
.relationship-boundary span { display: block; color: var(--muted); font-size: 12px; }
.calibration-status {
@@ -582,6 +589,8 @@ function _buildHTMLReport(chartData, extras) {
</table>` : '<p>当前导出未附带 Dasha 时间线。</p>'}
</section>
${_relationshipStrictNarrativeSection(relationshipNarrative)}
${_workflowReportSection(workflows)}
${_calibrationStatusSection(DASHA_SHADBALA_EXPORT_CALIBRATION_STATUS)}
@@ -609,6 +618,40 @@ function _buildHTMLReport(chartData, extras) {
</html>`;
}
function _relationshipStrictNarrativeSection(narrative) {
if (!narrative || typeof narrative !== 'object') return '';
const strengths = Array.isArray(narrative.strengths) ? narrative.strengths : [];
const risks = Array.isArray(narrative.risks) ? narrative.risks : [];
const boundaries = Array.isArray(narrative.boundaries) ? narrative.boundaries : [];
const markdown = typeof narrative.markdown === 'string' ? narrative.markdown : '';
const compactMarkdown = markdown
.replace(/^###\s+/gm, '')
.replace(/^\-\s+/gm, '')
.replace(/\n+/g, ' ')
.trim();
const showCaution = (
risks.some(item => String(item).includes('不能误读成接近结婚'))
|| boundaries.some(item => String(item).includes('不等于法律婚姻'))
);
return `<section class="relationship-strict-narrative">
<h2>婚恋严格裁决</h2>
<div class="relationship-hero">
<div>
<strong>${_h(narrative.headline || '婚恋 strict narrative 已接入导出主链。')}</strong>
<span>本段直接消费 relationship strict evidence,把 D9、UL、dual dasha 与 synastry taxonomy 的边界写进用户可见正文。</span>
</div>
<div class="relationship-status">strict narrative</div>
</div>
${showCaution ? `<div class="relationship-caution"><strong>防误判提示</strong><span>当前公开化/关系可见度候选不能被误读成接近法律婚姻;若 core marriage promise、dual dasha 或 external timing 仍未收敛,必须继续降置信度并保持 context-only 解释。</span></div>` : ''}
<div class="relationship-columns">
${_relationshipReportList('支持证据', strengths)}
${_relationshipReportList('需要观察', risks)}
${_relationshipReportList('边界条件', boundaries)}
</div>
${compactMarkdown ? `<div class="relationship-boundary"><span>${_h(compactMarkdown)}</span></div>` : ''}
</section>`;
}
function _calibrationStatusSection(status) {
return `<section>
<h2>高级技法校准状态</h2>
+120 -9
View File
@@ -1788,6 +1788,7 @@ function renderTrustCenterPanel() {
const pwa = getPWAStatus();
const terminology = getTerminologyModeOption();
const runtime = getRuntimeHealthStatus();
const vedastro = getVedAstroStatus();
const statusMessage = getTrustCenterStatusMessage(pwa, runtime);
return `
<section class="provenance-card trust-center-panel">
@@ -1802,6 +1803,7 @@ function renderTrustCenterPanel() {
${renderTrustStatus('术语', terminology.shortLabel, terminology.note)}
${renderTrustStatus('API', '127.0.0.1:5200', '本地 API 服务;不需要外部账号')}
${renderTrustStatus('运行体检', runtime.label, runtime.note)}
${renderVedAstroStatus(vedastro)}
</div>
${renderRuntimeHealthPanel(runtime)}
${renderValidationTransparencyPanel()}
@@ -2220,6 +2222,54 @@ function renderTrustStatus(label, value, note) {
`;
}
function getVedAstroStatus() {
const status = window.__jyotishVedAstroStatus;
if (!status) {
return {
label: '未检查',
note: '运行健康检查后读取 /api/vedastro/status;需要 VEDASTRO_API_ENDPOINT 才会连接外部雷达。',
tone: 'pending',
};
}
if (status.status === 'checking') {
return {
label: '检查中',
note: '正在读取 /api/vedastro/status。',
tone: 'pending',
};
}
if (status.status === 'live_ready') {
return {
label: 'live-ready',
note: `${status.endpoint_host || 'configured'} · VEDASTRO_ENABLE_NETWORK 已启用;外部证据仍只进 secondary context。`,
tone: 'ok',
};
}
if (status.status === 'network_execution_disabled') {
return {
label: '已配置/禁用网络',
note: `${status.endpoint_host || 'endpoint configured'} · 需显式启用 VEDASTRO_ENABLE_NETWORK。`,
tone: 'warn',
};
}
if (status.status === 'service_endpoint_not_configured') {
return {
label: '未配置',
note: '未设置 VEDASTRO_API_ENDPOINT;本地引擎继续可用,VedAstro 外部雷达保持 blocked。',
tone: 'pending',
};
}
return {
label: '需处理',
note: status.error || status.boundary || 'VedAstro 外部雷达状态不可用。',
tone: 'warn',
};
}
function renderVedAstroStatus(status = getVedAstroStatus()) {
return renderTrustStatus('VedAstro 外部雷达', status.label, status.note);
}
function getRuntimeHealthStatus() {
const health = window.__jyotishRuntimeHealth;
if (!health) {
@@ -2385,13 +2435,26 @@ async function runTrustCenterHealthCheck() {
['PWA 安装壳', getPWAStatus().label, getPWAStatus().note],
['本地 API 服务', '检查中', '正在请求 /api/health'],
['Technique catalog', '等待中', 'API 在线后检查能力目录'],
['VedAstro 外部雷达', '检查中', '正在请求 /api/vedastro/status'],
],
};
window.__jyotishVedAstroStatus = { status: 'checking' };
if (status) status.textContent = '正在运行健康检查...';
renderAll();
try {
const api = await window.JyotishAPI.getAPIHealth();
const audit = await window.JyotishAPI.getCapabilityAudit();
let vedastroStatus;
try {
vedastroStatus = await window.JyotishAPI.getVedAstroStatus();
} catch (error) {
vedastroStatus = {
status: 'unavailable',
error: error?.message || '/api/vedastro/status 不可用',
};
}
window.__jyotishVedAstroStatus = vedastroStatus;
const vedastro = getVedAstroStatus();
const surfaces = audit?.surfaces || {};
const registry = audit?.registry || {};
window.__jyotishRuntimeHealth = {
@@ -2406,11 +2469,17 @@ async function runTrustCenterHealthCheck() {
['Swiss Ephemeris', api.swisseph_available ? (api.swisseph_version || 'available') : 'unavailable', '当前本地 API 的星历运行时状态'],
['Ayanamsa', api.ayanamsa_default || 'lahiri', '当前健康检查声明的默认 sidereal 基准'],
['Technique catalog', `${registry.technique_count || 0} techniques`, `${surfaces.api_endpoint_count || 0} API endpoints 可被前端发现`],
['VedAstro 外部雷达', vedastro.label, vedastro.note],
['Desktop path', 'PWA now', 'Pake shell 可快速包 URL;Tauri sidecar 等 API 生命周期和签名策略确定后再落地。'],
],
};
if (status) status.textContent = '健康检查通过:本地 API 服务、能力目录和 PWA 安装壳状态已记录。';
} catch (error) {
window.__jyotishVedAstroStatus = {
status: 'unavailable',
error: '本地 API 未连接时无法读取 /api/vedastro/status。',
};
const vedastro = getVedAstroStatus();
window.__jyotishRuntimeHealth = {
status: 'warn',
checkedAt,
@@ -2418,6 +2487,7 @@ async function runTrustCenterHealthCheck() {
details: [
['PWA 安装壳', getPWAStatus().label, getPWAStatus().note],
['本地 API 服务', '未连接', error?.message || '请确认本地 API 服务正在 127.0.0.1:5200 运行'],
['VedAstro 外部雷达', vedastro.label, vedastro.note],
['启动路径', '普通用户启动路径', '按 README 先启动网页服务,再启动本地 API 服务。'],
['Packaging preflight', '待检查', '运行 python3 scripts/desktop_packaging_preflight.py 获取桌面包装前置结果。'],
],
@@ -5364,18 +5434,40 @@ function buildRelationshipReportTemplate(data = {}, deep = null) {
const hasDeep = Boolean(deep?.selfD9 || deep?.partnerD9 || kuja || dasha);
const spouseStatus = deep?.spouseStatus || null;
const ulDkTiming = deep?.ulDkTiming || null;
const strictNarrative = data.ai_prompt_pack?.evidence_snapshot?.relationship_narrative || data.relationship_narrative || deep?.relationshipNarrative || null;
const strictMarkdown = typeof strictNarrative?.markdown === 'string' ? strictNarrative.markdown : '';
const strictSecondaryContext = strictMarkdown.includes('public_formalization_candidate')
|| (Array.isArray(strictNarrative?.strengths) && strictNarrative.strengths.some(item => String(item).includes('public_formalization_candidate')))
|| (Array.isArray(strictNarrative?.boundaries) && strictNarrative.boundaries.some(item => String(item).includes('public_formalization_candidate')));
const hasPublicFormalizationCandidate = strictSecondaryContext;
const hasConflictWarning = (
Array.isArray(strictNarrative?.risks)
&& strictNarrative.risks.some(item => String(item).includes('不能误读成接近结婚'))
) || (
Array.isArray(strictNarrative?.boundaries)
&& strictNarrative.boundaries.some(item => String(item).includes('不等于法律婚姻'))
);
const approvedFlag = data.is_match_approved ?? data.is_approved;
const status = percentage >= 78 && approvedFlag !== false
? 'strong'
: percentage >= 58 && approvedFlag !== false
? 'workable'
: 'needs_context';
const statusLabel = {
const conservativePublicFormalizationStatus = hasPublicFormalizationCandidate && hasConflictWarning
? 'needs_context'
: null;
const status = hasPublicFormalizationCandidate && hasConflictWarning ? 'needs_context'
: conservativePublicFormalizationStatus
|| (percentage >= 78 && approvedFlag !== false
? 'strong'
: percentage >= 58 && approvedFlag !== false
? 'workable'
: 'needs_context');
const statusLabel = hasPublicFormalizationCandidate && hasConflictWarning
? '公开化候选,不等于婚姻逼近'
: {
strong: '高兼容,仍需完整复核',
workable: '可推进观察',
needs_context: '需谨慎综合判断',
}[status];
const headline = status === 'strong'
const headline = hasPublicFormalizationCandidate && hasConflictWarning
? '公开化候选浮现,但婚姻承诺与时机仍需保守复核。'
: status === 'strong'
? 'Ashtakoot 基础匹配较强,适合进入 D9、Kuja 与现实互动复核。'
: status === 'workable'
? '基础匹配有可用支撑,但关键风险位需要逐项确认。'
@@ -5385,6 +5477,10 @@ function buildRelationshipReportTemplate(data = {}, deep = null) {
if (dasha?.level === 'strong' || dasha?.level === 'supportive') strengths.push(dasha.note || dasha.label);
collectULDKTimingStrengths(ulDkTiming).forEach(item => strengths.push(item));
collectSpouseStatusStrengths(spouseStatus).forEach(item => strengths.push(item));
if (Array.isArray(strictNarrative?.strengths)) strictNarrative.strengths.forEach(item => strengths.push(item));
if (hasPublicFormalizationCandidate) {
strengths.push('public_formalization_candidate 说明当前更偏向公开化/关系可见度候选,而不是法律婚姻本身。');
}
if (!strengths.length) strengths.push('尚未出现足够稳定的强项,建议先补完整出生盘与 D9 复核。');
const risks = weakKutas.map(item => `${item.name} ${item.score}/${item.max}:需重点观察${relationshipKutaMeaning(item.name)}。`);
if (kuja?.verdict === 'hard' || kuja?.verdict === 'watch') risks.push(kuja.note || kuja.label);
@@ -5392,6 +5488,10 @@ function buildRelationshipReportTemplate(data = {}, deep = null) {
if ((data.exceptions || []).length) risks.push(`存在缓解条件:${(data.exceptions || []).join(';')}`);
collectULDKTimingRisks(ulDkTiming).forEach(item => risks.push(item));
collectSpouseStatusRisks(spouseStatus).forEach(item => risks.push(item));
if (Array.isArray(strictNarrative?.risks)) strictNarrative.risks.forEach(item => risks.push(item));
if (hasPublicFormalizationCandidate) {
risks.push('当前即便存在合盘支持与公开化候选,也不能误读成接近结婚;若 weak core promise、dual dasha 或 external timing 未收敛,仍应保持保守。');
}
if (!hasDeep) risks.push('当前只完成基础 Ashtakoot,缺少 D9、Kuja Dosha 与 Dasha 同步证据。');
if (!risks.length) risks.push('未见明显红旗,但仍需结合双方完整星盘与现实互动。');
const evidence = [
@@ -5405,7 +5505,7 @@ function buildRelationshipReportTemplate(data = {}, deep = null) {
return {
status,
statusLabel,
headline,
headline: strictNarrative?.headline || headline,
percentage,
evidence,
strongestKutas,
@@ -5416,11 +5516,17 @@ function buildRelationshipReportTemplate(data = {}, deep = null) {
'先确认双方出生时间精度,再复核 D9 与 7宫/7主。',
'把低分 Kuta 对应到现实互动议题,不用单一分数替代沟通。',
'若 Kuja 或 Dasha 出现压力,优先安排边界、节奏和冲突处理观察期。',
...(hasPublicFormalizationCandidate ? [
'若当前更偏向 public_formalization_candidate,请把它理解为关系公开化候选,而不是婚姻逼近。',
'先不要把高 Ashtakoot 分数翻译成婚姻逼近,应先复核 promise、dual dasha 与 external timing。',
] : []),
],
boundaries: [
'合盘报告只提供传统 Jyotish 证据,不替代个人选择。',
'总分通过不等于关系必然稳定;总分偏低也不等于关系不可经营。',
],
...(hasPublicFormalizationCandidate ? ['public_formalization_candidate 只表示公开化候选,不得越权抬升 legal_marriage,也不能误读成接近结婚。'] : []),
...((Array.isArray(strictNarrative?.boundaries) ? strictNarrative.boundaries : [])),
].slice(0, 5),
};
}
@@ -7226,12 +7332,17 @@ function buildExportExtras(sourceChart) {
const audit = computeAudit(planets, ascendant, av, validation);
const actionableContext = computeActionableContext(planets, ascendant);
const jaimini = _buildJaiminiModule(karaka, charaDasha, karakamsha);
const relationshipNarrative = sourceChart?.ai_prompt_pack?.evidence_snapshot?.relationship_narrative
|| sourceChart?.relationship_narrative
|| sourceChart?.modules?.relationship_strict_evidence?.user_narrative
|| null;
return {
dasha, yogas, vargas, aspects, jaimini,
nakshatraAdvanced, argala, tajika,
shadbala: sb, ashtakavarga: _buildAVModule(av),
panchanga, validation, audit, actionableContext,
relationship_narrative: relationshipNarrative,
provenance: sourceChart._client_audit?.provenance,
workflows: buildWorkflowExportExtras(sourceChart),
};
+126
View File
@@ -183,6 +183,50 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
if origin in allowed:
self.send_header('Access-Control-Allow-Origin', origin)
def _vedastro_status(self):
adapter = _load_local_module('vedastro_service_adapter')
endpoint = os.environ.get('VEDASTRO_API_ENDPOINT', '').strip()
network_flag = os.environ.get('VEDASTRO_ENABLE_NETWORK', '').strip().lower()
network_enabled = network_flag in {'1', 'true', 'yes'}
parsed = urlparse(endpoint) if endpoint else None
configured = bool(endpoint)
if not configured:
status = 'service_endpoint_not_configured'
elif not network_enabled:
status = 'network_execution_disabled'
else:
status = 'live_ready'
artifact_dir = getattr(adapter, 'ARTIFACT_DIR', None)
latest_artifact = None
if artifact_dir and os.path.isdir(artifact_dir):
artifacts = sorted(
(os.path.join(artifact_dir, name) for name in os.listdir(artifact_dir) if name.endswith('.json')),
key=lambda path: os.path.getmtime(path),
reverse=True,
)
if artifacts:
latest_artifact = os.path.relpath(artifacts[0], REPO_ROOT)
return {
'adapter': 'vedastro_service_adapter',
'backend': 'vedastro_service_adapter_candidate',
'status': status,
'configured': configured,
'network_enabled': network_enabled,
'endpoint_host': parsed.netloc if parsed else None,
'required_env': {
'endpoint': 'VEDASTRO_API_ENDPOINT',
'network': 'VEDASTRO_ENABLE_NETWORK',
'api_key_optional': 'VEDASTRO_API_KEY',
},
'live_profile': 'vedastro-live',
'transport': 'http_json_service_boundary',
'range_scan_role': adapter.VEDASTRO_CALCULATION_COVERAGE['range_scan_role'],
'official_events_builder_methods': adapter.VEDASTRO_CALCULATION_COVERAGE['official_events_builder_methods'],
'artifact_dir': 'scratch/local/vedastro_adapter',
'latest_artifact': latest_artifact,
'boundary': 'VedAstro is optional external timing evidence; local Jyotish gates remain authoritative.',
}
def do_OPTIONS(self):
self._json({})
@@ -213,6 +257,8 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
self._json(self._capability_audit())
elif path == '/api/technique_catalog':
self._json(self._technique_catalog())
elif path == '/api/vedastro/status':
self._json(self._vedastro_status())
elif path == '/api/real_case_revalidation':
self._json(self._real_case_revalidation())
else:
@@ -484,6 +530,65 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
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
headline = narrative.get('headline')
if not headline:
return html
def _escape(value):
return html_lib.escape(str(value or ''))
def _list_html(items):
if not isinstance(items, list) or not items:
return '<li>暂无补充。</li>'
return ''.join(f'<li>{_escape(item)}</li>' for item in items[:6])
risks = narrative.get("risks")
boundaries = narrative.get("boundaries")
caution_block = ''
if (
isinstance(risks, list)
and any('不能误读成接近结婚' in str(item) for item in risks)
) or (
isinstance(boundaries, list)
and any('不等于法律婚姻' in str(item) for item in boundaries)
):
caution_block = (
'<div class="relationship-caution" '
'style="margin:12px 0 16px;padding:12px 14px;border:1px solid #f3d19c;'
'border-left:4px solid #c67a00;border-radius:8px;background:#fff8ed;color:#7a4b00;">'
'<strong style="display:block;margin:0 0 6px;">Caution</strong>'
'<span style="display:block;font-size:13px;line-height:1.6;">'
'当前公开化/关系可见度候选不能被误读成接近法律婚姻;若 core marriage promise、dual dasha 或 external timing 仍未收敛,'
'必须继续降置信度并保持 context-only 解释。'
'</span>'
'</div>'
)
summary = (
'<section data-relationship-strict-narrative="true" '
'style="margin:24px 0;padding:16px;border:1px solid #d9dde8;border-radius:8px;'
'background:#fbfcff;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;">'
'<h2 style="margin:0 0 12px;font-size:20px;">Relationship Strict Narrative</h2>'
f'<p style="margin:0 0 12px;">{_escape(headline)}</p>'
f'{caution_block}'
'<div style="display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:16px;">'
'<div><strong>Strengths</strong><ul style="margin:8px 0 0 18px;padding:0;">'
f'{_list_html(narrative.get("strengths"))}</ul></div>'
'<div><strong>Risks</strong><ul style="margin:8px 0 0 18px;padding:0;">'
f'{_list_html(narrative.get("risks"))}</ul></div>'
'<div><strong>Boundaries</strong><ul style="margin:8px 0 0 18px;padding:0;">'
f'{_list_html(narrative.get("boundaries"))}</ul></div>'
'</div>'
'</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:
@@ -552,6 +657,10 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
html,
body.get('functional_benefic_malefic'),
)
html = self._inject_relationship_narrative_summary(
html,
body.get('relationship_narrative'),
)
fmt = body.get('format', 'html')
if fmt not in {'html', 'pdf'}:
raise BadRequest('format must be html or pdf')
@@ -1050,6 +1159,23 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
source='full_reading.modules.vivah_saham',
details=vivah.get('vivah_saham') if isinstance(vivah.get('vivah_saham'), dict) else vivah,
))
strict_relationship = full_modules.get('relationship_strict_evidence') if isinstance(full_modules, dict) else {}
user_narrative = strict_relationship.get('user_narrative') if isinstance(strict_relationship, dict) else {}
if isinstance(user_narrative, dict) and user_narrative.get('markdown'):
items.append(self._theme_evidence(
'Relationship-strict-narrative',
'Strict',
user_narrative.get('markdown'),
'neutral',
'strong',
source='full_reading.modules.relationship_strict_evidence.user_narrative',
details={
'headline': user_narrative.get('headline'),
'strengths': user_narrative.get('strengths', [])[:3],
'risks': user_narrative.get('risks', [])[:3],
'boundaries': user_narrative.get('boundaries', [])[:3],
},
))
return items
def _derived_career_evidence(self, chart_data, context):
+173 -2
View File
@@ -42,9 +42,11 @@ import os
import csv
import math
import sqlite3
import importlib.util
from datetime import datetime, timedelta
from typing import Dict, List
from tabulate import tabulate
from life_stage_hook import generate_life_stage_hooks
from ayanamsa_utils import (
AYANAMSA_DISPLAY_NAMES,
@@ -58,6 +60,7 @@ from ayanamsa_utils import (
# 路径常量
# ============================================================================
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
ROOT_DIR = os.path.dirname(SCRIPT_DIR)
HOME_DIR = os.path.expanduser('~')
CLAW_DIR = os.path.join(HOME_DIR, 'WorkBuddy', 'Claw')
DB_PATH = os.path.join(CLAW_DIR, 'vedic_astrology_validation.db')
@@ -378,7 +381,7 @@ def _get_temporary_relationship(planet1, planet2, planets_data):
"""Calculate Temporary Friendship (Tatkalika Maitri)."""
if not planets_data or planet1 not in planets_data or planet2 not in planets_data:
return 'NEUTRAL'
p1_sign = planets_data[planet1].get('sign')
p2_sign = planets_data[planet2].get('sign')
if not p1_sign or not p2_sign:
@@ -387,7 +390,7 @@ def _get_temporary_relationship(planet1, planet2, planets_data):
idx1 = SIGNS.index(p1_sign)
idx2 = SIGNS.index(p2_sign)
distance = (idx2 - idx1) % 12 + 1
# 2, 3, 4, 10, 11, 12 from planet are temporary friends
if distance in [2, 3, 4, 10, 11, 12]:
return 'FRIEND'
@@ -884,6 +887,7 @@ def _build_technique_audit_table(functional_layer, oracle_progress, modules):
ashtakavarga = modules.get('ashtakavarga') if isinstance(modules, dict) else {}
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 {}
rows = [
{
@@ -939,9 +943,131 @@ def _build_technique_audit_table(functional_layer, oracle_progress, modules):
f"Vimsopaka status={vimsopaka.get('status') if isinstance(vimsopaka, dict) else None}。"
),
})
event_judgement = relationship.get('event_judgement') if isinstance(relationship, dict) else {}
secondary_context = event_judgement.get('secondary_context') if isinstance(event_judgement, dict) else []
synastry_context = [item for item in (secondary_context or []) if isinstance(item, str) and item.startswith('synastry_')]
rows.append({
'technique': 'Relationship Synastry Taxonomy',
'status': 'used' if synastry_context else 'blocked',
'source': 'relationship strict workflow + synastry_relationship_bridge_v1',
'note': (
f"relationship secondary_context 中的 synastry 语义={synastry_context}; "
"compatibility support 表示匹配/延续性支持;"
"protective kuta support 表示防护型 Kuta 清洁度支持;"
"若 dual dasha / external timing / marriage convergence 冲突,"
"不得把这些支持越权解释成 legal marriage 的高置信度落地。"
),
})
return rows
def _build_relationship_narrative_payload(relationship_strict):
if not isinstance(relationship_strict, dict) or not relationship_strict:
return {
'headline': '婚恋严格裁决证据尚未完成,当前不能生成高严谨关系叙事。',
'strengths': [],
'risks': ['缺少 relationship strict workflow 的核心证据,婚恋正文需降级。'],
'boundaries': [
'未完成 D1 + D9 + UL + dual dasha 交叉前,不得把单一关系信号写成高置信度婚姻结论。',
],
'markdown': (
"### 婚恋严格裁决\n"
"- 当前缺少 relationship strict evidence,无法生成高严谨婚恋 narrative。\n"
"- 在 D1、D9、UL、Vimshottari 与 Narayana 未齐备前,应标记为 blocked 或降低置信度。"
),
}
event_judgement = relationship_strict.get('event_judgement') if isinstance(relationship_strict, 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 []
secondary_context = secondary_context if isinstance(secondary_context, list) else []
confidence_cap = relationship_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
synastry = present.get('synastry_relationship_support') if isinstance(present, dict) else {}
synastry_signals = synastry.get('signals') if isinstance(synastry, dict) else []
synastry_signals = synastry_signals if isinstance(synastry_signals, list) else []
strengths = []
risks = []
boundaries = []
if dominant_label == 'legal_marriage':
strengths.append('本轮严格裁决已把婚恋主标签抬到 legal_marriage,但仍需尊重时机与现实承诺层。')
elif dominant_label == 'public_formalization':
strengths.append('当前更偏向 public_formalization,表示关系可见度/公开化支持强于法律婚姻落地。')
elif 'public_formalization_candidate' in secondary_context:
strengths.append('当前更接近 public_formalization_candidate,表示公开化/关系可见度候选正在增强,但仍未达到法律婚姻落地。')
if 'jaimini_support' in secondary_context:
strengths.append('Jaimini 桥接已提供配偶征象支持,DK/UL 线索可用于补强婚恋叙事。')
if 'ul_support' in secondary_context:
strengths.append('Upapada Lagna 已进入严格证据,可作为关系承诺与婚姻叙事的辅助锚点。')
if 'synastry_support' in secondary_context:
strengths.append('合盘支持已进入婚恋主链,但它只说明关系兼容度有帮助,不能单独决定婚姻落地。')
if 'synastry_compatibility_support' in secondary_context:
strengths.append('protective kuta / compatibility support 说明部分 Kuta 与关系延续性维度较干净。')
if 'synastry_protective_kuta_support' in secondary_context:
strengths.append('protective kuta support 已被识别,可作为关系稳定性的次级支持语义。')
if 'synastry_exception_mitigated' in secondary_context:
strengths.append('存在 exception mitigation,说明部分 Dosha/不利匹配在传统规则里有缓解条件。')
if confidence_cap in {'low', 'blocked'}:
risks.append('当前 confidence cap 偏低,dual dasha / external timing / marriage convergence 至少有一层存在冲突或不足。')
if 'public_formalization_candidate' in secondary_context:
risks.append('当前虽更接近 public_formalization_candidate,但在 timing conflict 未解除前,不能误读成接近结婚。')
if missing:
risks.append(f"仍缺少关键层:{', '.join(str(item) for item in missing[:4])}。")
if 'virodhargala_obstruction' in secondary_context:
risks.append('第七宫 Argala 出现阻滞,关系推进可能伴随现实阻力或时间延后。')
if 'dignity_high_friction' in secondary_context:
risks.append('相关婚恋行星尊贵度摩擦较高,关系推进时更容易出现磨损与反复确认。')
if 'shadbala_component_gap' in secondary_context:
risks.append('Shadbala 六分量还存在缺口,关系强弱结论需继续保守处理。')
boundaries.append('婚恋高严谨模式至少需要 D1、D9、UL、Vimshottari 与 Narayana dual dasha 同时在场。')
boundaries.append('protective kuta support、Mahendra、Stree Deergha 等合盘细信号只能辅助,不得越权抬升 legal_marriage。')
boundaries.append('若 dual dasha、external timing 或 marriage convergence 冲突,必须明确降置信度,而不是把关系窗口包装成婚姻必然落地。')
if 'public_formalization_candidate' in secondary_context:
boundaries.append('public_formalization_candidate 只表示公开化候选,不等于法律婚姻,不能越权替代 legal_marriage。')
if synastry_signals:
boundaries.append(f"当前 synastry taxonomy 已命中 {', '.join(synastry_signals[:5])},但这些信号仍从属于 secondary-context。")
if not strengths:
strengths.append('当前婚恋 strict workflow 主要提供边界与缺口提示,尚未形成足够稳定的正向落地支持。')
if not risks:
risks.append('未见强烈负面冲突,但仍需用现实事件、D9 和 dual dasha 做最后复核。')
headline = (
'婚恋严格裁决已接入 synastry taxonomy,可把合盘支持翻译成次级关系语义。'
if 'synastry_support' in secondary_context
else '婚恋严格裁决已接入主链,但当前更依赖本命、D9 与时机层,而非合盘辅助。'
)
markdown_lines = [
'### 婚恋严格裁决',
f"- headline: {headline}",
f"- dominant_label: {dominant_label or 'none'}",
f"- confidence_cap: {confidence_cap}",
f"- secondary_context: {secondary_context}",
'- strengths:',
*[f" - {item}" for item in strengths],
'- risks:',
*[f" - {item}" for item in risks],
'- boundaries:',
*[f" - {item}" for item in boundaries],
]
return {
'headline': headline,
'strengths': strengths,
'risks': risks,
'boundaries': boundaries,
'markdown': "\n".join(markdown_lines),
}
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 {}
@@ -960,6 +1086,7 @@ def _build_ai_prompt_pack(report):
functional_layer = _functional_benefic_malefic_snapshot(planets, chart.get('ascendant', {}))
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'))
shadbala_ranking = []
for planet_name, pdata in sorted(
@@ -1031,6 +1158,7 @@ def _build_ai_prompt_pack(report):
'oracle_progress': oracle_progress,
'functional_benefic_malefic': functional_layer,
'technique_audit_table': technique_audit_table,
'relationship_narrative': relationship_narrative,
}
prompt_lines = [
@@ -1068,6 +1196,25 @@ def _build_ai_prompt_pack(report):
}
def _load_relationship_strict_collector():
try:
from mcp_server import _collect_strict_evidence as collector
return collector
except Exception:
mcp_path = os.path.join(ROOT_DIR, 'mcp_server.py')
if not os.path.exists(mcp_path):
raise
spec = importlib.util.spec_from_file_location("jyotish_root_mcp_server", mcp_path)
if spec is None or spec.loader is None:
raise ImportError(f"Unable to load mcp_server from {mcp_path}")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
collector = getattr(module, "_collect_strict_evidence", None)
if collector is None:
raise ImportError("mcp_server._collect_strict_evidence not found")
return collector
# ============================================================================
# 公共星盘计算(供 chart/shadbala/ashtakavarga 共用,v3.4提取)
# ============================================================================
@@ -4739,6 +4886,20 @@ def cmd_full_reading(args):
elapsed = round(time.time() - t0, 2)
module_count = len(report['modules'])
error_count = len(report['errors'])
# ── 生成动态引导 (Dynamic Hooks) ──
try:
report['dynamic_hooks'] = generate_life_stage_hooks(
planets=report['modules'].get('chart', {}).get('planets', {}),
asc_sign=report['modules'].get('chart', {}).get('ascendant', {}).get('sign', ''),
asc_idx=report['modules'].get('chart', {}).get('ascendant', {}).get('sign_idx', 0),
current_dasha=report['modules'].get('dasha', {}).get('current_dasha', {}),
narayana_dasha=report['modules'].get('narayana_dasha', {})
)
except Exception as e:
report['dynamic_hooks'] = []
report['errors'].append(f"hook_engine: {e}")
report['summary'] = {
'elapsed_seconds': elapsed,
'modules_computed': module_count,
@@ -4746,6 +4907,16 @@ def cmd_full_reading(args):
'status': 'complete' if error_count == 0 else f'{error_count} errors',
'next_step': '⭐ v6.1.6: full-reading 已输出 transit_multi_reference(四参考点) + dasa_convergence(五系统交叉) + yogini_dasha + ashtottari_dasha + kalachakra_dasha + d9_navamsa_expanded。AI必须使用四参考点分析Transit,Dasa预测必须标注多系统收敛等级。',
}
try:
relationship_strict_collector = _load_relationship_strict_collector()
report['modules']['relationship_strict_evidence'] = relationship_strict_collector('relationship', report)
report['modules']['relationship_strict_evidence']['user_narrative'] = _build_relationship_narrative_payload(
report['modules']['relationship_strict_evidence']
)
except Exception as e:
report['errors'].append(f"relationship-strict-evidence: {e}")
report['ai_prompt_pack'] = _build_ai_prompt_pack(report)
return report
+156
View File
@@ -0,0 +1,156 @@
#!/usr/bin/env python3
"""
Dynamic Life-Stage Hook Engine
Generates context-aware prompt suggestions based on the user's current Dasha and Transits.
"""
from typing import Any, Dict, List
# Core mapping for planets/houses to life themes
PLANET_THEMES = {
"Sun": ["career", "status", "authority", "father"],
"Moon": ["mind", "mother", "property", "emotional well-being"],
"Mars": ["property", "energy", "courage", "siblings", "conflicts"],
"Mercury": ["business", "communication", "intellect", "skills"],
"Jupiter": ["wealth", "children", "expansion", "luck", "wisdom"],
"Venus": ["marriage", "relationships", "luxury", "comfort"],
"Saturn": ["career", "delays", "discipline", "hard work", "karma"],
"Rahu": ["foreign affairs", "sudden events", "technology", "obsession"],
"Ketu": ["spirituality", "losses", "detachment", "isolation"]
}
HOUSE_THEMES = {
1: ["health", "self-development", "life path"],
2: ["wealth", "savings", "family"],
3: ["courage", "short trips", "communication"],
4: ["property", "mother", "home", "peace"],
5: ["investments", "children", "creativity"],
6: ["health issues", "debts", "enemies", "daily work"],
7: ["marriage", "business partnerships", "public relations"],
8: ["hidden matters", "sudden gains/losses", "research", "crises"],
9: ["fortune", "higher education", "long travel", "spirituality"],
10: ["career advancement", "public status", "leadership"],
11: ["gains", "networks", "profits", "elder siblings"],
12: ["foreign lands", "expenses", "isolation", "spirituality"]
}
def _get_lord_houses(planet: str, asc_idx: int) -> List[int]:
"""Return the houses owned by the given planet for the given ascendant index."""
SIGNS = ["Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo",
"Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces"]
SIGN_LORDS = {
"Aries": "Mars", "Taurus": "Venus", "Gemini": "Mercury", "Cancer": "Moon",
"Leo": "Sun", "Virgo": "Mercury", "Libra": "Venus", "Scorpio": "Mars",
"Sagittarius": "Jupiter", "Capricorn": "Saturn", "Aquarius": "Saturn", "Pisces": "Jupiter"
}
owned = []
for h in range(1, 13):
sign = SIGNS[(asc_idx + h - 1) % 12]
if SIGN_LORDS.get(sign) == planet:
owned.append(h)
return owned
def generate_life_stage_hooks(
planets: Dict[str, Any],
asc_sign: str,
asc_idx: int,
current_dasha: Dict[str, Any],
narayana_dasha: Dict[str, Any]
) -> List[Dict[str, str]]:
"""
Generate top 3 prompt hooks based on the current life stage.
"""
hooks = []
# 1. Antardasha (Sub-period) Hook
ad_lord = None
if isinstance(current_dasha, dict) and "antardasha" in current_dasha:
ad = current_dasha.get("antardasha")
if isinstance(ad, dict):
ad_lord = ad.get("lord")
elif isinstance(current_dasha, dict):
ad_lord = current_dasha.get("lord") # Fallback to MD lord
if ad_lord:
owned_houses = _get_lord_houses(ad_lord, asc_idx)
core_themes = []
for h in owned_houses:
core_themes.extend(HOUSE_THEMES.get(h, []))
if not core_themes:
core_themes = PLANET_THEMES.get(ad_lord, [])
if "career advancement" in core_themes or "wealth" in core_themes or ad_lord in ["Sun", "Jupiter"]:
hooks.append({
"type": "career_wealth",
"trigger": f"Current Antardasha Lord ({ad_lord})",
"question": f"🔮 测一测我在当前「{ad_lord}运」期间,是否有实质性的财务跃迁或事业升职机会?",
"rationale": f"系统检测到你正处于 {ad_lord} 主导的运势周期,该星体掌管你的核心资源/事业宫位,近期极易触发财务或职级变动。"
})
elif "marriage" in core_themes or "business partnerships" in core_themes or ad_lord in ["Venus", "Moon"]:
hooks.append({
"type": "relationship",
"trigger": f"Current Antardasha Lord ({ad_lord})",
"question": f"🔮 我的正缘大概会在什么时间点出现?当前运势对我的婚恋/合伙关系有什么影响?",
"rationale": f"系统检测到你正处于 {ad_lord} 运势周期,极易触发亲密关系或重要合伙人的变动,建议深度扫描婚恋应期。"
})
elif "foreign lands" in core_themes or "hidden matters" in core_themes or "health issues" in core_themes or ad_lord in ["Ketu", "Rahu"]:
hooks.append({
"type": "transition_healing",
"trigger": f"Current Antardasha Lord ({ad_lord})",
"question": f"🔮 我近期感到强烈的内耗/变动倾向,是否适合出国、换环境或进行重大人生断舍离?",
"rationale": f"当前 {ad_lord} 运势激活了隐秘/变动宫位,容易带来精神内耗或海外发展的契机,需要诊断当前的卡点。"
})
else:
# Fallback hook for Antardasha
hooks.append({
"type": "general_ad",
"trigger": f"Current Antardasha Lord ({ad_lord})",
"question": f"🔮 当前「{ad_lord}运」对我接下来的 1-2 年有什么本质性的影响?",
"rationale": f"你目前处于 {ad_lord} 掌管的次级运势中,深度解读该星体能帮你把握近期的核心节奏。"
})
# 2. Narayana Dasha Hook
if isinstance(narayana_dasha, dict):
nd_obj = narayana_dasha.get("current_dasha")
nd_sign = None
if isinstance(nd_obj, dict):
nd_sign = nd_obj.get("md", {}).get("sign")
elif isinstance(nd_obj, str):
nd_sign = nd_obj
if nd_sign:
hooks.append({
"type": "macro_trend",
"trigger": f"Narayana Dasha ({nd_sign})",
"question": f"🔮 我在当前的「{nd_sign}星座大运」中,最应该把精力聚焦在哪个领域才能利益最大化?",
"rationale": f"Jaimini 系统的 {nd_sign} 大运主轴已确认。顺应星座大运的能量流动,能帮你找到未来几年的阻力最小路径。"
})
# 3. Upcoming Transit (Gochar) Mock Hook (Requires actual ephemeris in full implementation)
# For now, we inject a generic but highly actionable transit hook.
hooks.append({
"type": "transit_alert",
"trigger": "Upcoming Major Transit",
"question": "🔮 未来半年内,木星或土星的换座/过宫,会给我带来哪些具体的机遇或危机?",
"rationale": "流年大星(木/土)的轨迹往往是触发本命盘事件的最后一把钥匙,提前观测能帮你避坑或抓红利。"
})
# Ensure we return exactly 3 top hooks
# Remove duplicates by type
seen_types = set()
unique_hooks = []
for h in hooks:
if h["type"] not in seen_types:
unique_hooks.append(h)
seen_types.add(h["type"])
# Always provide a fallback general reading option
unique_hooks.append({
"type": "general_reading",
"trigger": "User Preference",
"question": "🔮 跳过引导,我想查看完整的十年人生起伏与本命深度体检图谱。",
"rationale": "生成最全面的静态命运基调与大运概览。"
})
return unique_hooks[:3]
+14
View File
@@ -299,6 +299,20 @@ def test_full_reading_reports_ayanamsa_metadata_and_ai_prompt_pack() -> None:
assert strength_rows
assert strength_rows[0]["status"] == "used"
assert "Shadbala" in strength_rows[0]["note"]
relationship_synastry_rows = [row for row in audit_table if row["technique"] == "Relationship Synastry Taxonomy"]
assert relationship_synastry_rows
assert relationship_synastry_rows[0]["status"] in {"used", "blocked"}
assert "relationship secondary_context" in relationship_synastry_rows[0]["note"]
assert "compatibility support" in relationship_synastry_rows[0]["note"]
assert "protective kuta support" in relationship_synastry_rows[0]["note"]
relationship_narrative = prompt_pack["evidence_snapshot"]["relationship_narrative"]
assert relationship_narrative["headline"]
assert isinstance(relationship_narrative["strengths"], list)
assert isinstance(relationship_narrative["risks"], list)
assert isinstance(relationship_narrative["boundaries"], list)
assert relationship_narrative["markdown"]
assert "D9" in "".join(relationship_narrative["boundaries"])
assert "dual dasha" in relationship_narrative["markdown"]
oracle_progress = prompt_pack["evidence_snapshot"]["oracle_progress"]
assert oracle_progress["scope"] == "external_oracle_evidence_validation"
assert oracle_progress["valid_packets"] >= 4
+314 -2
View File
@@ -3,8 +3,16 @@
from __future__ import annotations
import sys
from pathlib import Path
from mcp_server import _collect_strict_evidence
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "scripts"))
import jyotish_engine # noqa: E402
def _base_relationship_result() -> dict:
return {
@@ -50,6 +58,7 @@ def test_relationship_jaimini_bridge_lifts_legal_marriage_label() -> None:
"jaimini_support",
"ul_support",
"virodhargala_obstruction",
"vedastro_range_scan_missing",
]
@@ -70,9 +79,62 @@ def test_relationship_jaimini_bridge_stays_context_only_when_d9_missing() -> Non
"jaimini_support",
"ul_support",
"virodhargala_obstruction",
"vedastro_range_scan_missing",
]
def test_relationship_jaimini_bridge_cannot_lift_legal_marriage_when_narayana_is_missing() -> None:
result = _base_relationship_result()
del result["modules"]["narayana_dasha"]
strict = _collect_strict_evidence("relationship", result)
assert strict["present_evidence"]["jaimini_marriage_support"] == {
"level": "moderate",
"signals": ["darakaraka_active", "dk_7h_link"],
"source": "jaimini_bridge_v1",
}
assert strict["event_judgement"]["dominant_label"] is None
assert "jaimini_support" in strict["event_judgement"]["secondary_context"]
assert "public_formalization_candidate" not in strict["event_judgement"]["secondary_context"]
def test_relationship_jaimini_bridge_uses_public_formalization_as_context_only_when_marriage_gate_is_open_but_label_support_missing() -> None:
result = _base_relationship_result()
del result["modules"]["vivah_saham"]
del result["modules"]["dasa_convergence"]
result["modules"]["external_activation"] = {
"evidence_ledger": [
{
"source": "vedastro_service_adapter_candidate",
"operation": "range_scan",
"domain": "marriage",
"event_id": "public_relationship_window",
"signal_key": "public_relationship_window",
"score": 78,
}
]
}
result["modules"]["synastry"] = {
"total_score": 28.0,
"max_score": 36.0,
"is_approved": True,
"additional_kutas": {
"Mahendra": "good",
"Vedha": "good",
"Rajju": {"result": "good", "group": None, "effect": ""},
},
}
strict = _collect_strict_evidence("relationship", result)
assert strict["event_judgement"]["dominant_label"] is None
assert "jaimini_support" in strict["event_judgement"]["secondary_context"]
assert "external_activation_support" in strict["event_judgement"]["secondary_context"]
assert "synastry_support" in strict["event_judgement"]["secondary_context"]
assert "public_formalization_candidate" in strict["event_judgement"]["secondary_context"]
def test_relationship_argala_bridge_uses_seventh_house_as_modifier_only() -> None:
result = _base_relationship_result()
@@ -135,7 +197,16 @@ def test_relationship_synastry_bridge_adds_context_and_small_score_bump_without_
assert strict["present_evidence"]["synastry_relationship_support"] == {
"level": "supportive",
"source": "synastry_relationship_bridge_v1",
"signals": ["ashtakoot_approved", "ashtakoot_high_score", "kuta_exception_clean"],
"signals": [
"ashtakoot_approved",
"ashtakoot_high_score",
"mahendra_support",
"stree_deergha_support",
"vedha_clean",
"rajju_clean",
"bad_constellations_clean",
"kuta_exception_clean",
],
"total_score": 29.0,
"approved": True,
}
@@ -188,11 +259,252 @@ def test_relationship_synastry_bridge_understands_nested_bad_constellations_and_
assert strict["present_evidence"]["synastry_relationship_support"] == {
"level": "moderate",
"source": "synastry_relationship_bridge_v1",
"signals": ["ashtakoot_approved", "exception_mitigated_match", "kuta_exception_clean"],
"signals": [
"ashtakoot_approved",
"exception_mitigated_match",
"mahendra_support",
"stree_deergha_support",
"vedha_clean",
"rajju_clean",
"bad_constellations_clean",
"kuta_exception_clean",
],
"total_score": 18.0,
"approved": True,
}
assert "synastry_support" in strict["event_judgement"]["secondary_context"]
assert "synastry_compatibility_support" in strict["event_judgement"]["secondary_context"]
assert "synastry_protective_kuta_support" in strict["event_judgement"]["secondary_context"]
assert "synastry_exception_mitigated" in strict["event_judgement"]["secondary_context"]
assert "synastry_kuta_exception_clean" in strict["event_judgement"]["secondary_context"]
assert "mahendra_support" in strict["event_judgement"]["secondary_context"]
assert "stree_deergha_support" in strict["event_judgement"]["secondary_context"]
assert "vedha_clean" in strict["event_judgement"]["secondary_context"]
assert "rajju_clean" in strict["event_judgement"]["secondary_context"]
assert "bad_constellations_clean" in strict["event_judgement"]["secondary_context"]
def test_relationship_synastry_exception_alone_cannot_create_false_lift_or_clean_kuta_context() -> None:
result = _base_relationship_result()
del result["modules"]["varga_full"]["D9_Navamsa"]
del result["modules"]["special_lagnas"]["Upapada_Lagna"]
del result["modules"]["vivah_saham"]
del result["modules"]["narayana_dasha"]
result["modules"]["synastry"] = {
"total_score": 14.0,
"max_score": 36.0,
"is_approved": False,
"exceptions": ["Nadi exception discussed but not fully mitigated."],
"additional_kutas": {
"Mahendra": "good",
"StreeDeergha": "good",
"Vedha": "bad",
"Rajju": {"result": "bad", "group": "same", "effect": "dosha"},
"BadConstellations": {"result": "bad", "issues": ["vedha clash"]},
},
}
strict = _collect_strict_evidence("relationship", result)
assert strict["present_evidence"]["synastry_relationship_support"] == {
"level": "none",
"source": "synastry_relationship_bridge_v1",
"signals": [
"exception_mitigated_match",
"mahendra_support",
"stree_deergha_support",
],
"total_score": 14.0,
"approved": False,
}
assert strict["event_judgement"]["dominant_label"] is None
assert "synastry_support" not in strict["event_judgement"]["secondary_context"]
assert "synastry_compatibility_support" not in strict["event_judgement"]["secondary_context"]
assert "synastry_protective_kuta_support" not in strict["event_judgement"]["secondary_context"]
assert "synastry_exception_mitigated" not in strict["event_judgement"]["secondary_context"]
assert "synastry_kuta_exception_clean" not in strict["event_judgement"]["secondary_context"]
assert "mahendra_support" not in strict["event_judgement"]["secondary_context"]
assert "rajju_clean" not in strict["event_judgement"]["secondary_context"]
def test_relationship_good_kutas_cannot_override_weak_core_marriage_promise() -> None:
result = _base_relationship_result()
del result["modules"]["vivah_saham"]
del result["modules"]["dasa_convergence"]
result["modules"]["jaimini"] = {
"darakaraka": {"planet": "Venus", "house": 3},
}
result["modules"]["synastry"] = {
"total_score": 31.0,
"max_score": 36.0,
"is_approved": True,
"additional_kutas": {
"Mahendra": "good",
"StreeDeergha": "good",
"Vedha": "good",
"Rajju": {"result": "good", "group": None, "effect": ""},
"BadConstellations": {"result": "good", "issues": []},
},
}
strict = _collect_strict_evidence("relationship", result)
assert strict["present_evidence"]["jaimini_marriage_support"] == {
"level": "weak",
"signals": ["darakaraka_active"],
"source": "jaimini_bridge_v1",
}
assert strict["present_evidence"]["synastry_relationship_support"]["level"] == "supportive"
assert strict["event_judgement"]["dominant_label"] is None
assert "synastry_support" in strict["event_judgement"]["secondary_context"]
assert "synastry_compatibility_support" in strict["event_judgement"]["secondary_context"]
assert "synastry_protective_kuta_support" in strict["event_judgement"]["secondary_context"]
assert "jaimini_support" not in strict["event_judgement"]["secondary_context"]
def test_relationship_protective_kutas_do_not_block_confidence_downgrade_when_timing_layers_conflict() -> None:
result = _base_relationship_result()
result["modules"]["synastry"] = {
"total_score": 31.0,
"max_score": 36.0,
"is_approved": True,
"additional_kutas": {
"Mahendra": "good",
"StreeDeergha": "good",
"Vedha": "good",
"Rajju": {"result": "good", "group": None, "effect": ""},
"BadConstellations": {"result": "good", "issues": []},
},
}
result["modules"]["external_activation"] = {
"evidence_ledger": [
{
"source": "vedastro_service_adapter_candidate",
"operation": "range_scan",
"domain": "career",
"event_id": "career_window_only",
"signal_key": "career_only_external_window",
"score": 74,
}
]
}
result["modules"]["dasa_convergence"] = {
"domain_activations": {
"career_status": {"convergence_level": "L4", "probability": "70-85%"}
}
}
strict = _collect_strict_evidence("relationship", result)
assert "synastry_protective_kuta_support" in strict["event_judgement"]["secondary_context"]
assert strict["event_judgement"]["dominant_label"] == "legal_marriage"
assert strict["confidence_cap"] == "low"
def test_relationship_narrative_payload_turns_synastry_taxonomy_into_user_readable_boundary() -> None:
strict = _collect_strict_evidence("relationship", _base_relationship_result())
strict["present_evidence"]["synastry_relationship_support"] = {
"level": "supportive",
"source": "synastry_relationship_bridge_v1",
"signals": [
"ashtakoot_approved",
"mahendra_support",
"stree_deergha_support",
"vedha_clean",
"rajju_clean",
],
"total_score": 29.0,
"approved": True,
}
strict["event_judgement"]["secondary_context"] = strict["event_judgement"]["secondary_context"] + [
"synastry_support",
"synastry_compatibility_support",
"synastry_protective_kuta_support",
]
strict["confidence_cap"] = "low"
payload = jyotish_engine._build_relationship_narrative_payload(strict)
assert "婚恋" in payload["headline"]
assert any("合盘" in item for item in payload["strengths"])
assert any("protective kuta" in item.lower() for item in payload["strengths"])
assert any("dual dasha" in item.lower() for item in payload["risks"])
assert any("legal_marriage" in item for item in payload["boundaries"])
assert "D9" in payload["markdown"]
assert "dual dasha" in payload["markdown"]
def test_relationship_narrative_payload_exposes_public_formalization_candidate_as_context_only() -> None:
strict = _collect_strict_evidence("relationship", _base_relationship_result())
strict["event_judgement"]["dominant_label"] = None
strict["event_judgement"]["secondary_context"] = [
"darakaraka_active",
"jaimini_support",
"ul_support",
"external_activation_support",
"public_formalization_candidate",
]
strict["present_evidence"].pop("vivah_saham", None)
strict["present_evidence"].pop("marriage_convergence", None)
strict["confidence_cap"] = "low"
payload = jyotish_engine._build_relationship_narrative_payload(strict)
assert any("public_formalization" in item for item in payload["strengths"])
assert any("不等于法律婚姻" in item for item in payload["boundaries"])
assert "public_formalization_candidate" in payload["markdown"]
assert "legal_marriage" in payload["markdown"]
def test_relationship_narrative_payload_for_public_formalization_candidate_with_timing_conflict_explicitly_warns_not_to_misread_as_near_marriage() -> None:
strict = _collect_strict_evidence("relationship", _base_relationship_result())
strict["event_judgement"]["dominant_label"] = None
strict["event_judgement"]["secondary_context"] = [
"darakaraka_active",
"jaimini_support",
"ul_support",
"external_activation_support",
"public_formalization_candidate",
]
strict["present_evidence"].pop("vivah_saham", None)
strict["present_evidence"].pop("marriage_convergence", None)
strict["confidence_cap"] = "low"
payload = jyotish_engine._build_relationship_narrative_payload(strict)
assert any("public_formalization_candidate" in item for item in payload["strengths"])
assert any("不能误读成接近结婚" in item for item in payload["risks"])
assert any("不等于法律婚姻" in item for item in payload["boundaries"])
assert "不能误读成接近结婚" in payload["markdown"]
def test_relationship_narrative_payload_does_not_translate_public_formalization_candidate_plus_synastry_support_plus_weak_core_promise_into_marriage_approach() -> None:
strict = _collect_strict_evidence("relationship", _base_relationship_result())
strict["event_judgement"]["dominant_label"] = None
strict["event_judgement"]["secondary_context"] = [
"darakaraka_active",
"ul_support",
"synastry_support",
"synastry_compatibility_support",
"public_formalization_candidate",
]
strict["present_evidence"]["jaimini_marriage_support"] = {
"level": "weak",
"signals": ["darakaraka_active"],
"source": "jaimini_bridge_v1",
}
strict["present_evidence"].pop("vivah_saham", None)
strict["present_evidence"].pop("marriage_convergence", None)
strict["confidence_cap"] = "low"
payload = jyotish_engine._build_relationship_narrative_payload(strict)
assert any("合盘支持已进入婚恋主链" in item for item in payload["strengths"])
assert any("公开化/关系可见度候选" in item for item in payload["strengths"])
assert any("不能误读成接近结婚" in item for item in payload["risks"])
assert any("不得越权抬升 legal_marriage" in item for item in payload["boundaries"])
assert "public_formalization_candidate" in payload["markdown"]
assert "legal_marriage" in payload["markdown"]
def test_relationship_dignity_guardrail_ignores_non_relevant_planets() -> None: