Productize VedAstro user range scan entry

This commit is contained in:
732642856
2026-06-29 07:51:00 +08:00
parent eb7ab36db1
commit 3161de68c0
12 changed files with 792 additions and 10 deletions
+206
View File
@@ -108,6 +108,30 @@ class _VedAstroStatusCaptureHandler(JyotishAPIHandler):
return json.loads(self.wfile.getvalue().decode('utf-8'))
class _PostCaptureHandler(JyotishAPIHandler):
def __init__(self, path: str, payload: dict) -> None:
raw = json.dumps(payload).encode('utf-8')
self.headers = _FakeHeaders({'Content-Length': str(len(raw))})
self.server = _FakeServer()
self.path = path
self.rfile = BytesIO(raw)
self.wfile = BytesIO()
self.status_code = None
self.response_headers = []
def send_response(self, code, message=None): # noqa: ANN001
self.status_code = code
def send_header(self, key, value): # noqa: ANN001
self.response_headers.append((key, value))
def end_headers(self):
return None
def payload(self) -> dict:
return json.loads(self.wfile.getvalue().decode('utf-8'))
def test_default_cors_origins_are_local_only() -> None:
assert 'http://localhost:3456' in DEFAULT_ALLOWED_ORIGINS
assert '*' not in DEFAULT_ALLOWED_ORIGINS
@@ -166,6 +190,42 @@ def test_vedastro_status_endpoint_exposes_safe_adapter_state(monkeypatch) -> Non
assert payload['live_profile'] == 'vedastro-live'
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)
handler = _PostCaptureHandler('/api/vedastro/range_scan', {
'domain': 'relationship',
'start_date': '2026-01-01',
'end_date': '2026-12-31',
'year': REDACTED_YEAR,
'month': 4,
'day': 17,
'hour': 14,
'minute': 49,
'second': 0,
'lat': 36.4467,
'lon': 114.2,
'tz': 8,
'ayanamsa_policy': 'lahiri',
'node_policy': 'mean',
})
handler.do_POST()
assert handler.status_code == 200
payload = handler.payload()
assert payload['success'] is True
assert payload['endpoint'] == 'vedastro_range_scan'
assert payload['ui_domain'] == 'relationship'
assert payload['adapter_domain'] == 'marriage'
assert payload['result']['status'] == 'service_endpoint_not_configured'
assert payload['result']['operation'] == 'range_scan'
assert payload['result']['request_preview']['year'] == REDACTED_YEAR
assert payload['result']['request_preview']['lat'] == 36.4467
assert payload['result']['request_preview']['domain'] == 'marriage'
assert payload['boundary'] == 'VedAstro range scan is optional external timing evidence; local Jyotish gates remain authoritative.'
@pytest.mark.parametrize(
('key', 'value', 'minimum', 'maximum'),
[
@@ -518,6 +578,124 @@ def test_report_artifact_can_render_functional_benefic_malefic_summary() -> None
assert '高严谨模式下必须叠加功能性吉凶星。' in html
def test_report_artifact_can_render_relationship_strict_narrative_summary() -> None:
handler = _handler()
result = handler._compute_report_artifact({
'format': 'html',
'name': 'relationship-strict-report',
'html': '<!doctype html><html><body><h1>Jyotish</h1></body></html>',
'relationship_narrative': {
'headline': '婚恋严格裁决已接入 synastry taxonomy,可把合盘支持翻译成次级关系语义。',
'strengths': ['合盘支持已进入婚恋主链,但它只说明关系兼容度有帮助。'],
'risks': ['当前 confidence cap 偏低,dual dasha / external timing / marriage convergence 存在冲突或不足。'],
'boundaries': ['婚恋高严谨模式至少需要 D1、D9、UL、Vimshottari 与 Narayana dual dasha 同时在场。'],
},
})
assert result['success'] is True
html = Path(result['html_path']).read_text(encoding='utf-8')
assert 'Relationship Strict Narrative' in html
assert 'synastry taxonomy' in html
assert 'dual dasha' in html
assert 'D1、D9、UL' in html
def test_report_artifact_relationship_strict_narrative_keeps_conflict_downgrade_language() -> None:
handler = _handler()
result = handler._compute_report_artifact({
'format': 'html',
'name': 'relationship-strict-conflict-report',
'html': '<!doctype html><html><body><h1>Jyotish</h1></body></html>',
'relationship_narrative': {
'headline': '婚恋 strict workflow 已识别支持层,但 timing conflict 仍要求降置信度。',
'strengths': ['D9、UL 与部分 synastry taxonomy 已在场。'],
'risks': ['dual dasha 与 external timing 发生冲突,不能把窗口直接抬成 legal marriage。'],
'boundaries': ['存在 timing conflict 时,最终婚恋 narrative 必须明确降置信度。'],
'markdown': '### 婚恋严格裁决\n- 当前 dual dasha 与 external timing 存在冲突,必须降置信度,不能把 supportive kuta 直接提升为 legal marriage。\n',
},
})
assert result['success'] is True
html = Path(result['html_path']).read_text(encoding='utf-8')
assert 'timing conflict' in html
assert 'dual dasha' in html
assert '降置信度' in html
def test_report_artifact_relationship_strict_narrative_surfaces_public_formalization_candidate_boundary() -> None:
handler = _handler()
result = handler._compute_report_artifact({
'format': 'html',
'name': 'relationship-strict-public-formalization-report',
'html': '<!doctype html><html><body><h1>Jyotish</h1></body></html>',
'relationship_narrative': {
'headline': '当前关系更接近 public_formalization candidate,而不是 legal marriage。',
'strengths': ['公开化/可见度支持正在升温,但仍属于 context-only 线索。'],
'risks': ['dual dasha 与 marriage convergence 还不足以把事件抬升为法律婚姻。'],
'boundaries': ['public_formalization_candidate 只表示公开化候选,不等于法律婚姻,不能越权替代 legal_marriage。'],
'markdown': '### 婚恋严格裁决\n- public_formalization_candidate 已进入 secondary-context,但仍不能替代 legal_marriage。\n',
},
})
assert result['success'] is True
html = Path(result['html_path']).read_text(encoding='utf-8')
assert 'public_formalization_candidate' in html
assert '不等于法律婚姻' in html
assert 'legal_marriage' in html
def test_report_artifact_relationship_strict_narrative_warns_public_formalization_candidate_not_to_be_misread_as_near_marriage() -> None:
handler = _handler()
result = handler._compute_report_artifact({
'format': 'html',
'name': 'relationship-strict-public-formalization-conflict-report',
'html': '<!doctype html><html><body><h1>Jyotish</h1></body></html>',
'relationship_narrative': {
'headline': '当前更接近 public_formalization candidate,但 timing conflict 仍然存在。',
'strengths': ['公开化候选正在形成,但仍只是 context-only 层。'],
'risks': ['当前 dual dasha / external timing 仍有冲突,不能误读成接近结婚。'],
'boundaries': ['public_formalization_candidate 不等于法律婚姻,不能越权替代 legal_marriage。'],
'markdown': '### 婚恋严格裁决\n- public_formalization_candidate 已进入 secondary-context,但当前 dual dasha 与 external timing 仍有冲突,不能误读成接近结婚,也不能替代 legal_marriage。\n',
},
})
assert result['success'] is True
html = Path(result['html_path']).read_text(encoding='utf-8')
assert 'public_formalization_candidate' in html
assert '不能误读成接近结婚' in html
assert 'legal_marriage' in html
def test_report_artifact_relationship_strict_narrative_surfaces_weak_core_promise_guardrail_for_public_formalization_candidate() -> None:
handler = _handler()
result = handler._compute_report_artifact({
'format': 'html',
'name': 'relationship-strict-weak-core-promise-report',
'html': '<!doctype html><html><body><h1>Jyotish</h1></body></html>',
'relationship_narrative': {
'headline': '当前更接近 public_formalization candidate,但 core marriage promise 仍偏弱。',
'strengths': [
'合盘支持已进入婚恋主链,但它只说明关系兼容度有帮助。',
'公开化/关系可见度候选正在增强,但仍未达到法律婚姻落地。',
],
'risks': ['当前 core marriage promise 偏弱,不能误读成接近结婚。'],
'boundaries': [
'protective kuta support 只能辅助,不得越权抬升 legal_marriage。',
'public_formalization_candidate 不等于法律婚姻。',
],
'markdown': '### 婚恋严格裁决\n- public_formalization_candidate 与 synastry_support 可以同时存在,但在 weak core marriage promise 下,仍不能写成婚姻逼近,也不能替代 legal_marriage。\n',
},
})
assert result['success'] is True
html = Path(result['html_path']).read_text(encoding='utf-8')
assert 'public_formalization_candidate' in html
assert '合盘支持已进入婚恋主链' in html
assert '不能误读成接近结婚' in html
assert 'legal_marriage' in html
assert 'relationship-caution' in html
def test_report_artifact_pdf_fallback_exposes_user_visible_delivery(monkeypatch) -> None:
class BrokenReportBuilder:
@staticmethod
@@ -832,9 +1010,37 @@ def test_thematic_report_derives_evidence_from_birth_payload() -> None:
}
assert 'chart' in marriage_sources
assert 'full_reading.modules.marriage_counting' in marriage_sources
assert 'full_reading.modules.relationship_strict_evidence.user_narrative' in marriage_sources
assert any(item['details'].get('derived') for item in result['themes']['career']['evidence'])
def test_thematic_report_derives_relationship_strict_narrative_evidence() -> None:
handler = _handler()
result = handler._compute_thematic_report({
'theme': ['marriage'],
'year': 1990,
'month': 1,
'day': 1,
'hour': 12,
'minute': 0,
'lat': 39.9,
'lon': 116.4,
'tz': 8,
})
assert result['success'] is True
marriage_evidence = result['themes']['marriage']['evidence']
strict_rows = [
item for item in marriage_evidence
if item['details'].get('source') == 'full_reading.modules.relationship_strict_evidence.user_narrative'
]
assert strict_rows
strict_note = strict_rows[0]['conclusion']
assert 'dual dasha' in strict_note
assert 'D9' in strict_note
assert 'legal_marriage' in strict_note or '婚恋' in strict_note
def test_fragment_audit_blocks_registry_surface_drift() -> None:
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'scripts'))
from audit_fragments import audit
+69
View File
@@ -987,6 +987,30 @@ def test_trust_center_surfaces_vedastro_adapter_status_without_endpoint_secret()
assert "secret/path" not in main
def test_trust_center_exposes_user_runnable_vedastro_range_scan() -> None:
main = (ROOT / "jyotish-app" / "main.js").read_text(encoding="utf-8")
api_bridge = (ROOT / "jyotish-app" / "api-bridge.js").read_text(encoding="utf-8")
public_bridge = (ROOT / "jyotish-app" / "public" / "api-bridge.js").read_text(encoding="utf-8")
for bridge in (api_bridge, public_bridge):
assert "runVedAstroRangeScan" in bridge
assert "/api/vedastro/range_scan" in bridge
for token in [
"renderVedAstroUserScanPanel",
"runVedAstroRangeScanFromPanel",
"vedastro-run-range-scan",
"vedastro-scan-domain",
"vedastro-scan-start",
"vedastro-scan-end",
"VedAstro Range Scan",
"modules.vedastro_range_scan_result",
"service_endpoint_not_configured",
"network_execution_disabled",
"外部证据只进 secondary context",
]:
assert token in main
def test_github_release_quality_gate_runs_browser_release_profile() -> None:
workflow = (ROOT / ".github" / "workflows" / "release-quality-gate.yml").read_text(encoding="utf-8")
for token in [
@@ -1686,6 +1710,8 @@ def test_provenance_panchanga_workspace_panel_is_productized() -> None:
"ulDkTiming",
"UL/DK 与关系时机",
"buildRelationshipReportTemplate",
"public_formalization_candidate",
"不能误读成接近结婚",
"relationshipKutaMeaning",
"renderRelationshipReport",
"renderRelationshipReportList",
@@ -1902,9 +1928,30 @@ def test_provenance_panchanga_workspace_panel_is_productized() -> None:
assert "_relationshipReportBullets" in export_js
assert "_relationshipReportList" in export_js
assert "_relationshipBoundary" in export_js
assert "_relationshipStrictNarrativeSection" in export_js
assert "relationship_report" in export_js
assert "relationship_narrative" in export_js
assert "relationship_narrative" in main
assert "strictNarrative" in main
assert "relationship-deliverable" in export_js
assert "relationship-evidence-grid" in export_js
assert "relationship-strict-narrative" in export_js
assert "relationship-caution" in export_js
assert "婚恋严格裁决" in export_js
assert "dual dasha" in export_js
def test_synastry_relationship_report_template_keeps_public_formalization_candidate_as_context_not_near_marriage() -> None:
main = read("main.js")
export_js = read("export.js")
html = read("index.html")
manifest = read("public/manifest.webmanifest")
sw = read("public/sw.js")
glossary = read("glossary.js")
assert "public_formalization_candidate" in main
assert "不能误读成接近结婚" in main
assert "不得越权抬升 legal_marriage" in main
assert "comparison-print-table" in export_js
assert "composite-print-grid" in export_js
assert "uldk-print-grid" in export_js
@@ -1985,6 +2032,28 @@ def test_provenance_panchanga_workspace_panel_is_productized() -> None:
assert "parseFloat($('birth-tz').value)" not in main
assert "window.confirm(`删除" in main
assert "window.confirm('清空本地星盘" in main
assert "高兼容,仍需完整复核" in main
assert "若当前更偏向 public_formalization_candidate,请把它理解为关系公开化候选,而不是婚姻逼近。" in main
assert "公开化候选浮现,但婚姻承诺与时机仍需保守复核。" in main
assert "公开化候选,不等于婚姻逼近" in main
assert "先不要把高 Ashtakoot 分数翻译成婚姻逼近,应先复核 promise、dual dasha 与 external timing。" in main
assert "status = hasPublicFormalizationCandidate && hasConflictWarning ? 'needs_context'" in main
def test_synastry_relationship_report_template_keeps_high_ashtakoot_public_formalization_and_weak_promise_case_fully_conservative() -> None:
main = read("main.js")
for token in [
"高兼容,仍需完整复核",
"public_formalization_candidate 说明当前更偏向公开化/关系可见度候选,而不是法律婚姻本身。",
"当前即便存在合盘支持与公开化候选,也不能误读成接近结婚;若 weak core promise、dual dasha 或 external timing 未收敛,仍应保持保守。",
"若当前更偏向 public_formalization_candidate,请把它理解为关系公开化候选,而不是婚姻逼近。",
"先不要把高 Ashtakoot 分数翻译成婚姻逼近,应先复核 promise、dual dasha 与 external timing。",
"public_formalization_candidate 只表示公开化候选,不得越权抬升 legal_marriage,也不能误读成接近结婚。",
"公开化候选浮现,但婚姻承诺与时机仍需保守复核。",
"公开化候选,不等于婚姻逼近",
]:
assert token in main
def test_mobile_layout_keeps_dense_sections_single_column() -> None:
+12
View File
@@ -20,6 +20,8 @@ def test_life_event_graph_folds_strict_evidence_and_vedastro_top_event() -> None
"ul_support",
"external_activation_support",
"synastry_support",
"synastry_compatibility_support",
"synastry_protective_kuta_support",
],
"primary_drivers": [
"marriage_convergence",
@@ -99,6 +101,16 @@ def test_life_event_graph_folds_strict_evidence_and_vedastro_top_event() -> None
"tags": ["marriage", "transit"],
"source": "vedastro_service_adapter_candidate",
}
assert {
"kind": "context",
"label": "synastry_compatibility_support",
"source": "event_judgement.secondary_context",
} in graph["event_nodes"]
assert {
"kind": "context",
"label": "synastry_protective_kuta_support",
"source": "event_judgement.secondary_context",
} in graph["event_nodes"]
def test_life_event_graph_is_returned_from_strict_relationship_evidence() -> None:
@@ -193,7 +193,7 @@ def test_vedastro_range_scan_unconfigured_still_returns_official_search_events_p
assert report["request_preview"]["official_request_profile"]["method"] == "POST"
assert report["request_preview"]["official_request_profile"]["headers"] == {"Content-Type": "application/json"}
assert report["request_preview"]["official_request_profile"]["body"]["Ayanamsa"] == "lahiri"
assert report["request_preview"]["official_request_profile"]["body"]["EventTagList"] == ["LendingMoney", "BorrowingMoney", "General"]
assert report["request_preview"]["official_request_profile"]["body"]["EventTagList"] == ["LendingMoney", "BorrowingMoney", "BuyingSelling", "General"]
assert "AtTime" not in report["request_preview"]["official_request_profile"]["body"]
assert report["request_preview"]["official_request_profile"]["body"]["StartTime"]["StdTime"] == "12:00 01/01/2026 +08:00"
assert report["request_preview"]["official_request_profile"]["body"]["EndTime"]["StdTime"] == "12:00 01/01/2031 +08:00"
@@ -230,7 +230,7 @@ def test_vedastro_service_adapter_posts_official_search_events_contract() -> Non
length = int(self.headers.get("Content-Length", "0"))
payload = json.loads(self.rfile.read(length).decode("utf-8"))
assert payload["Ayanamsa"] == "lahiri"
assert payload["EventTagList"] == ["Marriage", "General"]
assert payload["EventTagList"] == ["Marriage", "Personal", "General"]
assert payload["BirthTime"]["StdTime"] == "12:00 01/01/1990 +08:00"
assert payload["AtTime"]["StdTime"] == "12:00 01/01/2026 +08:00"
assert "StartTime" not in payload
@@ -369,7 +369,7 @@ def test_vedastro_service_adapter_can_normalize_mock_range_scan_response() -> No
length = int(self.headers.get("Content-Length", "0"))
payload = json.loads(self.rfile.read(length).decode("utf-8"))
assert payload["Ayanamsa"] == "lahiri"
assert payload["EventTagList"] == ["Marriage", "General"]
assert payload["EventTagList"] == ["Marriage", "Personal", "General"]
assert payload["BirthTime"]["StdTime"] == "12:00 01/01/1990 +08:00"
assert payload["StartTime"]["StdTime"] == "12:00 01/01/2026 +08:00"
assert payload["EndTime"]["StdTime"] == "12:00 01/01/2031 +08:00"
@@ -531,7 +531,7 @@ def test_vedastro_range_scan_records_hashes_and_artifact_path() -> None:
assert metadata["vedastro_event_method"] == "SearchEvents"
assert metadata["official_endpoint_path"] == "/Calculate/SearchEvents"
assert metadata["official_request_profile"]["method"] == "POST"
assert metadata["official_request_profile"]["body"]["EventTagList"] == ["Marriage", "General"]
assert metadata["official_request_profile"]["body"]["EventTagList"] == ["Marriage", "Personal", "General"]
assert metadata["official_request_profile_hash"]
assert metadata["allowlist_domain"] == "marriage"
assert metadata["allowlist_event_count"] == 1
@@ -691,6 +691,84 @@ def test_vedastro_service_adapter_applies_domain_allowlist_to_range_scan_noise()
assert report["evidence_ledger"][0]["event_id"] == "GocharJupiterIn7th"
def test_vedastro_service_adapter_preserves_match_metadata_for_official_tag_and_alias_hits() -> None:
class Handler(BaseHTTPRequestHandler):
def do_POST(self) -> None: # noqa: N802
response = {
"Status": "Pass",
"Payload": [
{
"Name": "GoodForMarriage",
"Nature": "Good",
"Description": "Marriage event support.",
"StartTime": "2026-05-01",
"EndTime": "2026-05-02",
"EventTags": ["Marriage"],
},
{
"Name": "PartnershipBlessingWindow",
"Nature": "Good",
"Description": "Spouse alignment and relationship blessing.",
"StartTime": "2026-05-03",
"EndTime": "2026-05-04",
"EventTags": ["Personal"],
},
],
}
body = json.dumps(response).encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, format: str, *args) -> None: # noqa: A003
return
server = HTTPServer(("127.0.0.1", 0), Handler)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
try:
env = os.environ.copy()
env["VEDASTRO_API_ENDPOINT"] = f"http://127.0.0.1:{server.server_port}/api"
env["VEDASTRO_ENABLE_NETWORK"] = "1"
completed = subprocess.run(
[
sys.executable,
"scripts/vedastro_service_adapter.py",
"--range-scan",
"--domain",
"marriage",
"--case",
"beijing_first_use_demo",
"--start-date",
"2026-01-01",
"--end-date",
"2026-12-31",
],
cwd=ROOT,
text=True,
capture_output=True,
timeout=120,
check=False,
env=env,
)
finally:
server.shutdown()
thread.join(timeout=5)
assert completed.returncode == 0, completed.stderr or completed.stdout
report = json.loads(completed.stdout)
assert report["event_count"] == 2
exact = {item["event_id"]: item for item in report["evidence_ledger"]}
assert exact["GoodForMarriage"]["matched_by"] == "official_tag"
assert exact["PartnershipBlessingWindow"]["matched_by"] == "alias"
assert exact["GoodForMarriage"]["confidence"] == "medium_high"
assert exact["PartnershipBlessingWindow"]["confidence"] == "low"
assert report["source_metadata"]["mapping_replay"]["match_counts"]["official_tag"] == 1
assert report["source_metadata"]["mapping_replay"]["match_counts"]["alias"] == 1
def test_vedastro_service_adapter_classifies_http_error() -> None:
class Handler(BaseHTTPRequestHandler):
def do_POST(self) -> None: # noqa: N802