${t('rect.events')} 0 ${t('rect.event.count')}
${t('rect.events.hint')}
@@ -90,6 +110,7 @@ function pctStyle(value) {
function bindEvents(container) {
const q = s => container.querySelector(s);
+ bindInterviewEvents(container);
q('#rect-add-btn').addEventListener('click', () => {
const dEl = q('#rect-event-date'), cEl = q('#rect-event-cat'), descEl = q('#rect-event-desc');
if (!dEl.value) { dEl.focus(); return; }
@@ -100,6 +121,68 @@ function bindEvents(container) {
q('#rect-run-btn').addEventListener('click', function () { handleRun(container, this); });
}
+function renderInterviewQuestion(question, lang) {
+ const prompt = lang === 'en' ? question.question_en : question.question_cn;
+ const label = lang === 'en' ? question.label_en : question.label_cn;
+ const examples = (question.examples_cn || []).join(' / ');
+ return `
+
+ ${escapeHtml(prompt)}
+ ${escapeHtml(label)} · ${escapeHtml(question.varga)}${examples ? ` · ${escapeHtml(examples)}` : ''}
+
+
+
+
+
+
+
+
+
+
+
`;
+}
+
+function bindInterviewEvents(container) {
+ container.querySelectorAll('.rect-interview-item').forEach(item => {
+ item.querySelectorAll('[data-rect-answer]').forEach(btn => {
+ btn.addEventListener('click', () => {
+ const answer = btn.dataset.rectAnswer;
+ item.querySelectorAll('[data-rect-answer]').forEach(other => other.classList.toggle('active', other === btn));
+ item.querySelector('.rect-answer-detail')?.classList.toggle('hidden', answer !== 'yes' && answer !== 'other');
+ rectInterviewAnswers[item.dataset.questionId] = {
+ answer,
+ category: item.dataset.category,
+ };
+ });
+ });
+ });
+ container.querySelector('#rect-import-interview')?.addEventListener('click', () => {
+ const events = collectInterviewEvents(container);
+ if (!events.length) {
+ const status = container.querySelector('#rect-interview-status');
+ if (status) status.textContent = '还没有可导入的“是”回答。';
+ return;
+ }
+ rectEvents = [...rectEvents, ...events];
+ renderEventList(container);
+ const status = container.querySelector('#rect-interview-status');
+ if (status) status.textContent = `已加入 ${events.length} 个事件。`;
+ });
+}
+
+function collectInterviewEvents(container) {
+ const answers = [...container.querySelectorAll('.rect-interview-item')].map(item => {
+ const saved = rectInterviewAnswers[item.dataset.questionId] || {};
+ return {
+ ...saved,
+ category: saved.category || item.dataset.category,
+ date: item.querySelector('.rect-answer-date')?.value || '',
+ note: item.querySelector('.rect-answer-note')?.value || '',
+ };
+ });
+ return rectificationInterviewAnswersToEvents(answers);
+}
+
function renderEventList(container) {
const listEl = container.querySelector('#rect-event-list');
const countEl = container.querySelector('#rect-event-count');
@@ -400,4 +483,4 @@ function showOffsetDetail(container, r, base) {
det.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
-export function initRectification() { rectEvents = []; }
+export function initRectification() { rectEvents = []; rectInterviewAnswers = {}; }
diff --git a/jyotish-app/style.css b/jyotish-app/style.css
index 9041d2ac..c8f6cf08 100644
--- a/jyotish-app/style.css
+++ b/jyotish-app/style.css
@@ -4626,6 +4626,97 @@ body { font-family: var(--font-body); background: var(--bg-page); color: var(--t
.rect-config-row { display: flex; gap: 16px; flex-wrap: wrap; }
.rect-config-row .form-group { flex: 1; min-width: 140px; }
+.rect-interview { margin-bottom: 16px; }
+.rect-interview-head {
+ display: flex;
+ justify-content: space-between;
+ gap: 16px;
+ align-items: flex-start;
+ margin-bottom: 12px;
+}
+.rect-interview-head p {
+ color: var(--text-secondary);
+ font-size: 12px;
+ line-height: 1.6;
+}
+.rect-interview-head span {
+ flex: 0 0 auto;
+ padding: 3px 8px;
+ border-radius: 6px;
+ background: var(--bg-surface);
+ border: 1px solid var(--border-light);
+ color: var(--text-muted);
+ font-size: 10px;
+}
+.rect-interview-list { display: grid; gap: 8px; }
+.rect-interview-item {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) auto;
+ gap: 10px;
+ padding: 10px;
+ border: 1px solid var(--border-light);
+ border-radius: var(--radius-sm);
+ background: var(--bg-surface);
+}
+.rect-interview-copy strong {
+ display: block;
+ color: var(--text-heading);
+ font-size: 13px;
+}
+.rect-interview-copy span {
+ display: block;
+ color: var(--text-muted);
+ font-size: 11px;
+ margin-top: 3px;
+}
+.rect-answer-group {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+.rect-answer-group button,
+.rect-secondary-btn {
+ padding: 6px 10px;
+ border: 1px solid var(--border-card);
+ border-radius: 6px;
+ background: var(--bg-white);
+ color: var(--text-secondary);
+ cursor: pointer;
+ font-family: var(--font-body);
+ font-size: 12px;
+}
+.rect-answer-group button.active,
+.rect-answer-group button:hover,
+.rect-secondary-btn:hover {
+ border-color: var(--accent);
+ color: var(--text-heading);
+}
+.rect-answer-detail {
+ grid-column: 1 / -1;
+ display: grid;
+ grid-template-columns: 160px minmax(0, 1fr);
+ gap: 8px;
+}
+.rect-answer-detail input {
+ padding: 8px 10px;
+ border: 1px solid var(--border-input);
+ border-radius: 6px;
+ background: var(--bg-white);
+ color: var(--text-heading);
+ font-family: var(--font-body);
+ font-size: 12px;
+}
+.rect-interview-actions {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ margin-top: 12px;
+}
+.rect-interview-actions span {
+ color: var(--text-secondary);
+ font-size: 12px;
+}
+
.rect-events { margin-bottom: 16px; }
.rect-count { font-size: 12px; color: var(--text-secondary); font-weight: 400; }
.rect-hint { font-size: 12px; color: var(--text-secondary); margin: 4px 0 12px; }
diff --git a/mcp_server.py b/mcp_server.py
index 6e639c15..10b4e20a 100644
--- a/mcp_server.py
+++ b/mcp_server.py
@@ -1830,33 +1830,32 @@ def _maybe_attach_vedastro_evidence(
return result
try:
- from vedastro_service_adapter import run_range_scan_for_case
+ from vedastro_evidence_orchestrator import orchestrate_vedastro_evidence
except Exception:
return result
start_date, end_date = _default_vedastro_scan_window(transit_date)
- case = {
- "year": year,
- "month": month,
- "day": day,
- "hour": hour,
- "minute": minute,
- "second": 0,
- "lat": lat,
- "lon": lon,
- "tz": tz,
- "ayanamsa_policy": (
- _safe_get(result, "meta", "ayanamsa")
- or _safe_get(result, "chart", "ayanamsa")
- or "lahiri"
- ),
- "node_policy": node_mode or "mean",
- }
- scan_result = run_range_scan_for_case(
- case,
- vedastro_domain,
- start_date,
- end_date,
+ scan_result = orchestrate_vedastro_evidence(
+ {
+ "year": year,
+ "month": month,
+ "day": day,
+ "hour": hour,
+ "minute": minute,
+ "second": 0,
+ "lat": lat,
+ "lon": lon,
+ "tz": tz,
+ "ayanamsa_policy": (
+ _safe_get(result, "meta", "ayanamsa")
+ or _safe_get(result, "chart", "ayanamsa")
+ or "lahiri"
+ ),
+ "node_policy": node_mode or "mean",
+ },
+ route=route,
+ start_date=start_date,
+ end_date=end_date,
case_id=f"strict_workflow_{route}",
)
if not isinstance(scan_result, dict):
diff --git a/progress.md b/progress.md
index 1fb3e518..20ff2234 100644
--- a/progress.md
+++ b/progress.md
@@ -481,3 +481,14 @@
- 实测真相:官方公共 MCP `initialize` 与 `tools/list` 已通过真实 HTTP 返回;Python bridge 的 `DasaAtTime` 通过 positional args 已成功返回 Vimshottari 快照;`GetCharaDasaAtTime` 的 bound-method 形状已进一步探明,其真实桥接仍需按 positional contract 单独收口,但不再属于“完全未知调用面”。
- 本地主线真相同步:补齐 `references/event_judgment_career.md`,并将其挂入 `SKILL.md`、`references/quick-reference-guide.md` 与 `docs/research/ACTIVE_FRONTS.md`。这意味着 career 线不再缺“专用裁决骨架”,后续尾巴集中在裁决细化、报告渲染与 oracle 闭环,而非入口缺失。
- Fresh oracle status re-check:`python3 scripts/oracle_closure_master_dashboard.py --format json` 当前显示 `12` 个总任务中 `8` 个已 external_verified、`4` 个未闭合;未闭合核心已收缩到 `tajika_sahams` 前线,而非 Dasha/Shadbala 主前线。`python3 scripts/public_benchmark_dashboard.py --format json` 当前显示 `valid_packets=5`、`ready_for_calibration=5`、`production_tuning_allowed=false`,因此仍不能宣称全局 oracle 已封顶。
+
+## 2026-06-29T08:20:00+08:00 - 项目进度深度检测
+
+- 按用户要求深度检测当前印度占星项目进度;读取 `task_plan.md`、`findings.md`、`progress.md`、`ACTIVE_FRONTS.md`、VedAstro parity/fast-path 文档与 oracle benchmark inventory。
+- Git 状态确认:当前分支 `codex/release-hygiene-ci` 与远端同步,HEAD 为 `d0af9f4 Productize VedAstro user range scan entry`,工作区无未提交 diff。
+- 能力面确认:`python3 scripts/audit_capabilities.py --mode validate` 通过,注册表 `89` 个技法中 `10 complete / 79 covered / 0 missing / 0 partial / 0 not-integrated`。
+- 碎片面确认:`python3 scripts/audit_fragments.py --strict` 通过,`engine_command_count=37`、`api_endpoint_count=43`、`candidate_count=0`、`untracked_count=0`。
+- Oracle 面确认:`oracle_closure_master_dashboard` 显示 `12` 个任务中 `8` 个 external_verified、`4` 个 open;Dasha 与 Shadbala 当前可声明对应 front closure,但全局 oracle closure 仍为 false,生产调参仍不允许。
+- VedAstro 面确认:当前已进入可点击用户入口和 adapter/provenance 阶段;官方 MCP/Python bridge 有真实可达记录,REST range scan 的官方 endpoint smoke 仍依赖 `VEDASTRO_API_ENDPOINT` 与 `VEDASTRO_ENABLE_NETWORK=1`。
+- 质量门确认:`python3 scripts/run_quality_gate.py --profile quick --skip-frontend-runtime` 通过,聚焦集合 `297 passed`;`env -u VEDASTRO_API_ENDPOINT -u VEDASTRO_ENABLE_NETWORK python3 scripts/run_quality_gate.py --profile vedastro-live` 通过但返回受控 `blocked`,不得宣称官方实网 VedAstro range scan 已闭环。
+- 当前最值钱下一步:继续推进 VedAstro range scan 的官方样本调参和 EventTagList 映射质量,同时补 Tajika/Sahams 4 个未闭合外部 oracle 任务;之后再处理 Vimsopaka 高阶语义映射、Life Event Graph v1 产品化和报告渲染 polish。
diff --git a/scripts/jyotish_api_server.py b/scripts/jyotish_api_server.py
index 088706e6..787ec60b 100644
--- a/scripts/jyotish_api_server.py
+++ b/scripts/jyotish_api_server.py
@@ -14,7 +14,6 @@ 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
@@ -57,7 +56,7 @@ def _attach_vedastro_main_entry_overview(chart_result, birth_payload):
return chart_result
try:
- adapter = _load_local_module('vedastro_service_adapter')
+ orchestrator = _load_local_module('vedastro_evidence_orchestrator')
except Exception:
return chart_result
@@ -66,9 +65,7 @@ def _attach_vedastro_main_entry_overview(chart_result, birth_payload):
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 = {
+ modules['vedastro_range_scan_result'] = orchestrator.orchestrate_vedastro_evidence({
'year': birth_payload.get('year'),
'month': birth_payload.get('month'),
'day': birth_payload.get('day'),
@@ -80,87 +77,7 @@ def _attach_vedastro_main_entry_overview(chart_result, birth_payload):
'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,
- }
+ }, route='overview', reference_date=reference_date, case_id='api_chart')
return chart_result
diff --git a/scripts/run_quality_gate.py b/scripts/run_quality_gate.py
index 26f40de6..02d34682 100644
--- a/scripts/run_quality_gate.py
+++ b/scripts/run_quality_gate.py
@@ -13,6 +13,10 @@ import tempfile
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
+sys.path.insert(0, str(ROOT / "scripts"))
+from local_env import load_local_env
+
+load_local_env(ROOT)
APP = ROOT / "jyotish-app"
PYTHON = sys.executable
diff --git a/scripts/vedastro_evidence_orchestrator.py b/scripts/vedastro_evidence_orchestrator.py
new file mode 100644
index 00000000..6cebe9a0
--- /dev/null
+++ b/scripts/vedastro_evidence_orchestrator.py
@@ -0,0 +1,133 @@
+#!/usr/bin/env python3
+"""Shared VedAstro evidence orchestration layer.
+
+The orchestrator does not expose hundreds of VedAstro nodes to users. It picks
+the smallest useful domain scan set for the current route and delegates the
+actual external boundary to ``vedastro_service_adapter``.
+"""
+
+from __future__ import annotations
+
+from datetime import datetime, timedelta
+from typing import Any
+
+try:
+ from scripts.vedastro_service_adapter import (
+ VEDASTRO_CALCULATION_COVERAGE,
+ run_range_scan_for_case,
+ )
+except ModuleNotFoundError: # pragma: no cover - script execution path
+ from vedastro_service_adapter import VEDASTRO_CALCULATION_COVERAGE, run_range_scan_for_case
+
+
+ROUTE_DOMAIN_MAP = {
+ "relationship": ["marriage"],
+ "marriage": ["marriage"],
+ "career": ["career"],
+ "finance": ["wealth"],
+ "wealth": ["wealth"],
+ "rectification": ["marriage", "career", "wealth"],
+ "timing": ["career", "marriage", "wealth"],
+ "general": ["career", "marriage", "wealth"],
+ "overview": ["career", "marriage", "wealth"],
+}
+
+
+def _default_window(reference_date: str | None, days: int = 180) -> tuple[str, str]:
+ raw = str(reference_date or datetime.utcnow().strftime("%Y-%m-%d"))[:10]
+ try:
+ start = datetime.strptime(raw, "%Y-%m-%d").date()
+ except ValueError:
+ return raw, raw
+ return start.isoformat(), (start + timedelta(days=days)).isoformat()
+
+
+def _normalize_case(birth_payload: dict[str, Any]) -> dict[str, Any]:
+ return {
+ "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_policy")
+ or birth_payload.get("ayanamsa")
+ or "lahiri",
+ "node_policy": birth_payload.get("node_policy")
+ or birth_payload.get("node_mode")
+ or birth_payload.get("nodeMode")
+ or "mean",
+ }
+
+
+def orchestrate_vedastro_evidence(
+ birth_payload: dict[str, Any],
+ *,
+ route: str = "general",
+ reference_date: str | None = None,
+ start_date: str | None = None,
+ end_date: str | None = None,
+ case_id: str = "vedastro_orchestrator",
+) -> dict[str, Any]:
+ domains = ROUTE_DOMAIN_MAP.get(route, ROUTE_DOMAIN_MAP["general"])
+ window_start, window_end = (start_date, end_date) if start_date and end_date else _default_window(reference_date)
+ case = _normalize_case(birth_payload)
+ domain_reports: dict[str, Any] = {}
+ evidence_ledger: list[dict[str, Any]] = []
+ top_events_by_domain: dict[str, Any] = {}
+ domain_statuses: dict[str, Any] = {}
+ domain_event_counts: dict[str, int] = {}
+ available = False
+ first_reason = None
+
+ for domain in domains:
+ report = run_range_scan_for_case(
+ case,
+ domain,
+ str(window_start),
+ str(window_end),
+ case_id=f"{case_id}_{domain}",
+ )
+ domain_reports[domain] = report
+ domain_statuses[domain] = report.get("status")
+ domain_event_counts[domain] = int(report.get("event_count", 0) or 0)
+ available = available or bool(report.get("available"))
+ first_reason = first_reason or report.get("reason")
+ if isinstance(report.get("top_event"), dict):
+ top_events_by_domain[domain] = report["top_event"]
+ for event in report.get("evidence_ledger") or []:
+ if isinstance(event, dict):
+ evidence_ledger.append(event)
+
+ status = "ok" if any(item == "ok" for item in domain_statuses.values()) else next(iter(domain_statuses.values()), "blocked")
+ return {
+ "backend": "vedastro_service_adapter_candidate",
+ "available": available,
+ "status": status,
+ "operation": "range_scan",
+ "domain": domains[0] if len(domains) == 1 else "overview",
+ "route": route,
+ "event_count": len(evidence_ledger),
+ "top_event": next(iter(top_events_by_domain.values()), None),
+ "top_events_by_domain": top_events_by_domain,
+ "evidence_ledger": evidence_ledger,
+ "reason": None if status == "ok" else first_reason,
+ "domain_reports": domain_reports,
+ "source_metadata": {
+ "auto_ingested_by": "VedAstroEvidenceOrchestrator",
+ "strategy": "minimal_route_scoped_orchestration",
+ "node_coverage": {
+ "strategy": "domain_scoped_range_scan",
+ "official_calculation_coverage": VEDASTRO_CALCULATION_COVERAGE,
+ "selected_domains": domains,
+ "not_user_exposed": True,
+ },
+ "route": route,
+ "scan_window": {"start_date": str(window_start), "end_date": str(window_end)},
+ "domain_statuses": domain_statuses,
+ "domain_event_counts": domain_event_counts,
+ },
+ }
diff --git a/scripts/vedastro_service_adapter.py b/scripts/vedastro_service_adapter.py
index dc5be9f9..812c8144 100644
--- a/scripts/vedastro_service_adapter.py
+++ b/scripts/vedastro_service_adapter.py
@@ -9,6 +9,7 @@ workspace can evolve from research notes to an executable adapter contract.
from __future__ import annotations
import argparse
+import http.client
import hashlib
import json
import os
@@ -19,8 +20,14 @@ from typing import Any
from urllib import request, error
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
+
ROOT = Path(__file__).resolve().parents[1]
+load_local_env(ROOT)
PARITY_CASES = {
@@ -493,9 +500,11 @@ def _range_scan_preview(case: dict[str, Any], domain: str, start_date: str, end_
"start_date": start_date,
"end_date": end_date,
"event_model": "vedastro_events_at_range_candidate",
+ "search_mode": "single_point",
**case,
}
preview["official_request_profile"] = _build_official_search_events_profile(preview)
+ preview["live_sampling_request_profile"] = _build_live_sampling_search_events_profile(preview)
return preview
@@ -559,6 +568,28 @@ def _build_official_search_events_profile(request_preview: dict[str, Any]) -> di
}
+def _build_live_sampling_search_events_profile(request_preview: dict[str, Any]) -> dict[str, Any]:
+ case = dict(request_preview)
+ case["tz"] = _normalize_tz(case)
+ body = {
+ "BirthTime": _time_json_from_case(case, f"{case['year']:04d}-{case['month']:02d}-{case['day']:02d}"),
+ "Ayanamsa": str(case.get("ayanamsa_policy") or "lahiri"),
+ "EventTagList": OFFICIAL_RANGE_SCAN_EVENT_TAGS.get(str(request_preview.get("domain") or ""), ["General"]),
+ "AtTime": _time_json_from_case(case, str(request_preview["start_date"])),
+ }
+ headers: dict[str, str] = {"Content-Type": "application/json"}
+ api_key = os.environ.get("VEDASTRO_API_KEY", "").strip()
+ if api_key:
+ headers["x-api-key"] = api_key
+ return {
+ "profile_version": f"{OFFICIAL_SEARCH_EVENTS_PROFILE_VERSION}_live_sampling",
+ "endpoint_path": OFFICIAL_SEARCH_EVENTS_ENDPOINT_PATH,
+ "method": OFFICIAL_SEARCH_EVENTS_METHOD,
+ "headers": headers,
+ "body": body,
+ }
+
+
def _external_technique_preview(
case: dict[str, Any],
domain: str,
@@ -685,9 +716,15 @@ def _normalize_range_scan_success(
attempt_count: int = 1,
retry_error_codes: list[int] | None = None,
) -> dict[str, Any]:
- # Handle actual VedAstro response format: {"Status": "Pass", "Payload": [...]}
+ # Handle actual VedAstro response formats:
+ # {"Status": "Pass", "Payload": {"SearchEvents": [...]}}
+ # {"Status": "Pass", "Payload": [...]}
if payload.get("Status") == "Pass":
- events = payload.get("Payload", [])
+ payload_body = payload.get("Payload", [])
+ if isinstance(payload_body, dict):
+ events = payload_body.get("SearchEvents", [])
+ else:
+ events = payload_body
else:
# Fallback to local stub format if not VedAstro format
events = payload.get("events", [])
@@ -716,10 +753,12 @@ def _normalize_range_scan_success(
for index, event in enumerate(events, start=1):
if not isinstance(event, dict):
continue
- # VedAstro uses "Name" for event id, and "EventTags" for tags
+ # VedAstro uses "Name" for event id, and may expose tags as EventTags, tags, or Tag.
event_id = event.get("Name") or event.get("id") or event.get("name") or f"event_{index}"
- tags = event.get("EventTags") or event.get("tags") or []
- if not isinstance(tags, list):
+ tags = event.get("EventTags") or event.get("tags") or event.get("Tag") or []
+ if isinstance(tags, str):
+ tags = [part.strip() for part in tags.split(",") if part.strip()]
+ elif not isinstance(tags, list):
tags = []
tag_set = {str(tag) for tag in tags}
matched_by = "rejected"
@@ -865,7 +904,12 @@ def _source_metadata(endpoint: str) -> dict[str, Any]:
def _post_json(endpoint: str, request_preview: dict[str, Any]) -> dict[str, Any] | str:
- official_request_profile = request_preview.get("official_request_profile") if isinstance(request_preview, dict) else None
+ official_request_profile = None
+ if isinstance(request_preview, dict):
+ official_request_profile = (
+ request_preview.get("live_sampling_request_profile")
+ or request_preview.get("official_request_profile")
+ )
request_url = endpoint
headers = {"Content-Type": "application/json"}
vedastro_payload = request_preview
@@ -913,6 +957,97 @@ def _post_json_with_retry(endpoint: str, request_preview: dict[str, Any]) -> tup
return {}, max_attempts, retry_error_codes
+def _iter_sample_dates(start_date: str, end_date: str) -> list[str]:
+ from datetime import datetime, timedelta
+
+ start = datetime.strptime(start_date, "%Y-%m-%d").date()
+ end = datetime.strptime(end_date, "%Y-%m-%d").date()
+ if end <= start:
+ return [start_date]
+ span_days = max((end - start).days, 1)
+ step_days = max(1, span_days // 11)
+ dates: list[str] = []
+ current = start
+ while current <= end:
+ dates.append(current.isoformat())
+ current += timedelta(days=step_days)
+ if dates[-1] != end.isoformat():
+ dates.append(end.isoformat())
+ return dates
+
+
+def _merge_range_scan_reports(
+ reports: list[dict[str, Any]],
+ endpoint: str,
+ base_preview: dict[str, Any],
+) -> dict[str, Any]:
+ if not reports:
+ return _normalize_range_scan_success({"Status": "Pass", "Payload": []}, endpoint, base_preview)
+
+ deduped: dict[tuple[str, str, str], dict[str, Any]] = {}
+ for report in reports:
+ for item in report.get("evidence_ledger", []):
+ key = (
+ str(item.get("event_id") or ""),
+ str(item.get("start") or ""),
+ str(item.get("end") or ""),
+ )
+ existing = deduped.get(key)
+ existing_score = existing.get("signal_lift", 0) if existing else -1
+ score = item.get("signal_lift", 0)
+ if existing is None or score > existing_score:
+ deduped[key] = item
+
+ evidence_ledger = list(deduped.values())
+ top_event = None
+ if evidence_ledger:
+ top = max(
+ evidence_ledger,
+ key=lambda item: (
+ item.get("signal_lift") if isinstance(item.get("signal_lift"), (int, float)) else 0,
+ item.get("confidence") == "high",
+ ),
+ )
+ top_event = {
+ "event_id": top.get("event_id"),
+ "signal_key": top.get("signal_key"),
+ "signal_label": top.get("signal_label"),
+ "signal_family": top.get("signal_family"),
+ "score": top.get("score"),
+ "start": top.get("start"),
+ "end": top.get("end"),
+ "tags": top.get("tags") or [],
+ }
+
+ metadata = dict(reports[-1].get("source_metadata") or {})
+ metadata["sampling_mode"] = "at_time_sweep"
+ metadata["sample_dates"] = [report.get("request_preview", {}).get("start_date") for report in reports]
+ metadata["sample_count"] = len(reports)
+ metadata["raw_event_count"] = sum(int((report.get("source_metadata") or {}).get("raw_event_count", 0)) for report in reports)
+ metadata["filtered_event_count"] = len(evidence_ledger)
+ metadata["allowlist_event_count"] = len(evidence_ledger)
+ metadata["attempt_count"] = sum(int((report.get("source_metadata") or {}).get("attempt_count", 1)) for report in reports)
+ retry_codes: list[int] = []
+ for report in reports:
+ retry_codes.extend(list((report.get("source_metadata") or {}).get("retry_error_codes") or []))
+ metadata["retry_error_codes"] = retry_codes
+
+ result = {
+ "backend": "vedastro_service_adapter_candidate",
+ "available": True,
+ "status": "ok",
+ "operation": "range_scan",
+ "domain": base_preview.get("domain"),
+ "request_preview": base_preview,
+ "event_count": len(evidence_ledger),
+ "top_event": top_event,
+ "evidence_ledger": evidence_ledger,
+ "source_metadata": metadata,
+ }
+ result["source_metadata"]["artifact_path"] = _write_artifact(result)
+ return result
+
+
def run_case(case_id: str) -> dict[str, Any]:
if case_id not in PARITY_CASES:
return {
@@ -1007,46 +1142,55 @@ def _run_range_scan_case(case: dict[str, Any], domain: str, start_date: str, end
"source_metadata": _source_metadata(endpoint),
}
- try:
- payload, attempt_count, retry_error_codes = _post_json_with_retry(endpoint, request_preview)
- except error.HTTPError as exc:
- return {
- "backend": "vedastro_service_adapter_candidate",
- "available": False,
- "status": "http_error",
- "reason": f"VedAstro range scan HTTP error: {exc.code}",
- "request_preview": request_preview,
- "source_metadata": _source_metadata(endpoint),
- }
- except error.URLError as exc:
- return {
- "backend": "vedastro_service_adapter_candidate",
- "available": False,
- "status": "network_error",
- "reason": f"VedAstro range scan network error: {exc.reason}",
- "request_preview": request_preview,
- "source_metadata": _source_metadata(endpoint),
- }
- except (TimeoutError, socket.timeout):
- return {
- "backend": "vedastro_service_adapter_candidate",
- "available": False,
- "status": "timeout",
- "reason": "VedAstro range scan timed out",
- "request_preview": request_preview,
- "source_metadata": _source_metadata(endpoint),
- }
- except json.JSONDecodeError:
- return {
- "backend": "vedastro_service_adapter_candidate",
- "available": False,
- "status": "invalid_json",
- "reason": "VedAstro range scan received non-JSON response",
- "request_preview": request_preview,
- "source_metadata": _source_metadata(endpoint),
- }
+ sample_dates = _iter_sample_dates(start_date, end_date)
+ reports: list[dict[str, Any]] = []
+ for sample_date in sample_dates:
+ sample_preview = dict(request_preview)
+ sample_preview["start_date"] = sample_date
+ sample_preview["end_date"] = sample_date
+ sample_preview["official_request_profile"] = _build_official_search_events_profile(sample_preview)
+ sample_preview["live_sampling_request_profile"] = _build_live_sampling_search_events_profile(sample_preview)
+ try:
+ payload, attempt_count, retry_error_codes = _post_json_with_retry(endpoint, sample_preview)
+ except error.HTTPError as exc:
+ return {
+ "backend": "vedastro_service_adapter_candidate",
+ "available": False,
+ "status": "http_error",
+ "reason": f"VedAstro range scan HTTP error: {exc.code}",
+ "request_preview": sample_preview,
+ "source_metadata": _source_metadata(endpoint),
+ }
+ except (error.URLError, http.client.RemoteDisconnected) as exc:
+ return {
+ "backend": "vedastro_service_adapter_candidate",
+ "available": False,
+ "status": "network_error",
+ "reason": f"VedAstro range scan network error: {getattr(exc, 'reason', str(exc))}",
+ "request_preview": sample_preview,
+ "source_metadata": _source_metadata(endpoint),
+ }
+ except (TimeoutError, socket.timeout):
+ return {
+ "backend": "vedastro_service_adapter_candidate",
+ "available": False,
+ "status": "timeout",
+ "reason": "VedAstro range scan timed out",
+ "request_preview": sample_preview,
+ "source_metadata": _source_metadata(endpoint),
+ }
+ except json.JSONDecodeError:
+ return {
+ "backend": "vedastro_service_adapter_candidate",
+ "available": False,
+ "status": "invalid_json",
+ "reason": "VedAstro range scan received non-JSON response",
+ "request_preview": sample_preview,
+ "source_metadata": _source_metadata(endpoint),
+ }
+ reports.append(_normalize_range_scan_success(payload, endpoint, sample_preview, attempt_count, retry_error_codes))
- return _normalize_range_scan_success(payload, endpoint, request_preview, attempt_count, retry_error_codes)
+ return _merge_range_scan_reports(reports, endpoint, request_preview)
def run_range_scan(case_id: str, domain: str, start_date: str, end_date: str) -> dict[str, Any]:
diff --git a/task_plan.md b/task_plan.md
index abaca0f1..9bfefc24 100644
--- a/task_plan.md
+++ b/task_plan.md
@@ -8,6 +8,18 @@
- 阶段:用户端信任层与运行可用性
- 本轮最高优先级:在已完成首次使用引导、真实浏览器首跑冒烟、AI/API key 安全提示、导出失败恢复后,推进星历后端抽象可行性,把 SwissEph/WASM/xalen/VedAstro/PyJHora 的替换边界变成可检测资产。
+## 最短路径封口方案(2026-06-29)
+
+后续不再扩新功能,优先只做 4 条封口线:
+
+1. `relationship adjudicator` 回归闭环
+2. `Vimsopaka + Functional Role` 闭环
+3. `Shadbala / Dasha / JHora` oracle 批处理闭环
+4. `VedAstro strict ingestion` 最小诚实接入
+
+执行顺序与任务拆分见:
+- `/Users/wuyongnaren/Documents/印度占星/docs/superpowers/plans/2026-06-29-shortest-path-closure-plan.md`
+
## 执行原则
1. 每次实现前先扫描本地碎片、`references/open_source_sources/*`、现有测试与产品差距矩阵。
diff --git a/tests/test_mcp_strict_workflow_relationship.py b/tests/test_mcp_strict_workflow_relationship.py
index 4508ac3f..9af530c6 100644
--- a/tests/test_mcp_strict_workflow_relationship.py
+++ b/tests/test_mcp_strict_workflow_relationship.py
@@ -544,6 +544,24 @@ def test_relationship_dignity_guardrail_uses_relevant_planets_only() -> None:
assert "dignity_supportive_recovery" in strict["event_judgement"]["secondary_context"]
+def test_relationship_dignity_guardrail_treats_great_friend_on_relevant_planet_as_supportive_context() -> None:
+ result = _base_relationship_result()
+ result["modules"]["chart"] = {
+ "ascendant": {"sign": "Leo"},
+ "planets": {
+ "Venus": {"status": "极友(Great Friend)"},
+ "Jupiter": {"status": "中性(Neutral)"},
+ "Saturn": {"status": "中性(Neutral)"},
+ },
+ }
+
+ strict = _collect_strict_evidence("relationship", result)
+
+ assert strict["present_evidence"]["dignity_guardrail"]["status"] == "caution"
+ assert strict["present_evidence"]["dignity_guardrail"]["score_delta"] == 3
+ assert "dignity_supportive_friendship" in strict["event_judgement"]["secondary_context"]
+
+
def test_relationship_caps_confidence_when_provided_shadbala_components_are_incomplete() -> None:
result = _base_relationship_result()
result["modules"]["dasa_convergence"]["domain_activations"]["marriage_partnership"] = {
diff --git a/tests/test_vedastro_evidence_orchestrator.py b/tests/test_vedastro_evidence_orchestrator.py
new file mode 100644
index 00000000..4069780d
--- /dev/null
+++ b/tests/test_vedastro_evidence_orchestrator.py
@@ -0,0 +1,57 @@
+from __future__ import annotations
+
+from pathlib import Path
+
+
+ROOT = Path(__file__).resolve().parents[1]
+
+
+def test_vedastro_evidence_orchestrator_routes_to_minimal_domain_set(monkeypatch) -> None:
+ from scripts import vedastro_evidence_orchestrator as orchestrator
+
+ calls = []
+
+ def fake_scan(case, domain, start_date, end_date, case_id):
+ calls.append((domain, start_date, end_date, case_id, case["year"]))
+ return {
+ "backend": "vedastro_service_adapter_candidate",
+ "available": True,
+ "status": "ok",
+ "operation": "range_scan",
+ "domain": domain,
+ "event_count": 1,
+ "evidence_ledger": [{"domain": domain, "event_id": f"{domain}_event"}],
+ "source_metadata": {"endpoint_host": "api.vedastro.org"},
+ }
+
+ monkeypatch.setattr(orchestrator, "run_range_scan_for_case", fake_scan)
+
+ result = orchestrator.orchestrate_vedastro_evidence(
+ {
+ "year": REDACTED_YEAR,
+ "month": 4,
+ "day": 17,
+ "hour": 14,
+ "minute": 49,
+ "lat": 36.42,
+ "lon": 114.2,
+ "tz": 8.0,
+ },
+ route="relationship",
+ reference_date="2026-06-29",
+ )
+
+ assert [call[0] for call in calls] == ["marriage"]
+ assert result["source_metadata"]["auto_ingested_by"] == "VedAstroEvidenceOrchestrator"
+ assert result["source_metadata"]["node_coverage"]["strategy"] == "domain_scoped_range_scan"
+ assert result["event_count"] == 1
+
+
+def test_api_and_mcp_use_shared_vedastro_evidence_orchestrator() -> None:
+ api = (ROOT / "scripts" / "jyotish_api_server.py").read_text(encoding="utf-8")
+ mcp = (ROOT / "mcp_server.py").read_text(encoding="utf-8")
+
+ assert "vedastro_evidence_orchestrator" in api
+ assert "orchestrate_vedastro_evidence" in api
+ assert "vedastro_evidence_orchestrator" in mcp
+ assert "orchestrate_vedastro_evidence" in mcp
diff --git a/tests/test_vedastro_external_technique_evidence.py b/tests/test_vedastro_external_technique_evidence.py
index 59b836fb..9d127d87 100644
--- a/tests/test_vedastro_external_technique_evidence.py
+++ b/tests/test_vedastro_external_technique_evidence.py
@@ -9,7 +9,9 @@ import subprocess
import sys
from copy import deepcopy
from pathlib import Path
+from unittest import mock
+import mcp_server
from mcp_server import _collect_strict_evidence
@@ -44,7 +46,8 @@ def test_vedastro_adapter_declares_external_technique_evidence_policy() -> None:
def test_vedastro_adapter_builds_external_technique_preview_without_network() -> None:
env = os.environ.copy()
env["VEDASTRO_API_ENDPOINT"] = "https://example.invalid/vedastro"
- env.pop("VEDASTRO_ENABLE_NETWORK", None)
+ env["VEDASTRO_ENABLE_NETWORK"] = "0"
+ env["JYOTISH_SKIP_LOCAL_ENV"] = "1"
completed = subprocess.run(
[
@@ -228,3 +231,114 @@ def test_strict_workflow_marks_vedastro_range_scan_used_without_score_or_label_o
assert strict["event_judgement"]["score"] == base["event_judgement"]["score"]
assert strict["event_judgement"]["dominant_label"] == base["event_judgement"]["dominant_label"]
assert strict["event_judgement"]["payout_label"] == base["event_judgement"]["payout_label"]
+
+
+def test_strict_workflow_auto_ingests_live_vedastro_range_scan_for_finance_route() -> None:
+ fake_range_scan = {
+ "backend": "vedastro_service_adapter_candidate",
+ "available": True,
+ "status": "ok",
+ "operation": "range_scan",
+ "domain": "wealth",
+ "event_count": 1,
+ "evidence_ledger": [
+ {
+ "source": "vedastro_service_adapter_candidate",
+ "operation": "range_scan",
+ "domain": "wealth",
+ "event_id": "GocharJupiterIn11th",
+ "signal_label": "Jupiter in 11th gains window",
+ "score": 76,
+ "tags": ["wealth", "transit"],
+ }
+ ],
+ "source_metadata": {"sampling_mode": "at_time_sweep"},
+ }
+
+ with mock.patch.object(mcp_server, "_run_engine") as run_engine, mock.patch.object(
+ mcp_server,
+ "_maybe_attach_vedastro_evidence",
+ ) as attach_vedastro:
+ run_engine.return_value = {"modules": _finance_modules()}
+ attach_vedastro.return_value = {
+ "modules": {
+ **_finance_modules(),
+ "vedastro_range_scan_result": fake_range_scan,
+ }
+ }
+
+ result = mcp_server.strict_workflow(
+ question="今年的财富机会怎么样?",
+ year=REDACTED_YEAR,
+ month=4,
+ day=17,
+ hour=14,
+ minute=49,
+ lat=36.42,
+ lon=114.2,
+ tz=8.0,
+ age=33,
+ transit_date="2026-06-29",
+ node_mode="mean",
+ )
+
+ strict = result["strict_workflow"]
+ attach_vedastro.assert_called_once()
+ assert strict["present_evidence"]["external_activation"]["level"] == "moderate"
+ assert "external_activation_support" in strict["event_judgement"]["secondary_context"]
+ assert "vedastro_range_scan_missing" not in strict["event_judgement"]["secondary_context"]
+
+
+def test_strict_workflow_auto_ingests_live_vedastro_external_technique_as_context_only() -> None:
+ fake_external_technique = {
+ "backend": "vedastro_service_adapter_candidate",
+ "available": True,
+ "status": "ok",
+ "operation": "calculation_method",
+ "role": "external_technique_evidence",
+ "domain": "wealth",
+ "evidence_ledger": [
+ {
+ "source": "vedastro_service_adapter_candidate",
+ "operation": "calculation_method",
+ "role": "external_technique_evidence",
+ "domain": "wealth",
+ "method": "CalculateShadbala",
+ "api_endpoint": "Calculate/Shadbala",
+ "status": "ok",
+ "summary": "VedAstro Shadbala node returned as external evidence.",
+ }
+ ],
+ }
+
+ with mock.patch.object(mcp_server, "_run_engine") as run_engine, mock.patch.object(
+ mcp_server,
+ "_maybe_attach_vedastro_evidence",
+ ) as attach_vedastro:
+ run_engine.return_value = {"modules": _finance_modules()}
+ attach_vedastro.return_value = {
+ "modules": {
+ **_finance_modules(),
+ "external_technique_evidence": fake_external_technique,
+ }
+ }
+
+ result = mcp_server.strict_workflow(
+ question="我的财务今年如何?",
+ year=REDACTED_YEAR,
+ month=4,
+ day=17,
+ hour=14,
+ minute=49,
+ lat=36.42,
+ lon=114.2,
+ tz=8.0,
+ age=33,
+ transit_date="2026-06-29",
+ node_mode="mean",
+ )
+
+ strict = result["strict_workflow"]
+ assert strict["present_evidence"]["external_technique_evidence"]["level"] == "context_only"
+ assert strict["present_evidence"]["external_technique_evidence"]["methods"] == ["CalculateShadbala"]
+ assert "external_technique_evidence" in strict["event_judgement"]["secondary_context"]
diff --git a/tests/test_vedastro_range_scan_replay.py b/tests/test_vedastro_range_scan_replay.py
index 255a0ca4..39428bd1 100644
--- a/tests/test_vedastro_range_scan_replay.py
+++ b/tests/test_vedastro_range_scan_replay.py
@@ -2,6 +2,7 @@ from __future__ import annotations
import json
from pathlib import Path
+from unittest import mock
from scripts import vedastro_service_adapter
@@ -73,3 +74,74 @@ def test_replay_stats_track_zero_event_domain_and_rejected_alias_noise() -> None
assert replay["zero_event_domains"] == ["wealth"]
assert replay["match_counts"]["rejected"] == 1
assert replay["recommended_allowlist_candidates"] == []
+
+
+def test_normalize_official_live_payload_supports_nested_searchevents_and_tag_string() -> None:
+ payload = {
+ "Status": "Pass",
+ "Payload": {
+ "SearchEvents": [
+ {
+ "Name": "BadLunarMonthForBuilding",
+ "Nature": "Bad",
+ "Description": "Building work should be avoided in this lunar month.",
+ "StartTime": {"StdTime": "12:00 01/01/2026 +08:00"},
+ "EndTime": {"StdTime": "12:00 01/01/2026 +08:00"},
+ "Tag": "Building,General",
+ }
+ ]
+ },
+ }
+
+ report = vedastro_service_adapter._normalize_range_scan_success( # noqa: SLF001
+ payload,
+ "https://api.vedastro.org/api",
+ _request_preview("career"),
+ )
+
+ assert report["event_count"] == 1
+ assert report["evidence_ledger"][0]["event_id"] == "BadLunarMonthForBuilding"
+ assert report["evidence_ledger"][0]["matched_by"] == "official_tag"
+ assert report["evidence_ledger"][0]["matched_terms"] == ["Building"]
+ assert report["evidence_ledger"][0]["tags"] == ["Building", "General"]
+
+
+def test_run_range_scan_case_refreshes_live_sampling_request_profile_for_each_sample_date() -> None:
+ case = vedastro_service_adapter.PARITY_CASES["beijing_first_use_demo"]
+ captured_at_times: list[str] = []
+
+ def fake_post_json_with_retry(endpoint: str, request_preview: dict[str, object]):
+ live_profile = request_preview.get("live_sampling_request_profile") or {}
+ body = live_profile.get("body") if isinstance(live_profile, dict) else {}
+ at_time = body.get("AtTime") if isinstance(body, dict) else {}
+ std_time = at_time.get("StdTime") if isinstance(at_time, dict) else None
+ captured_at_times.append(std_time)
+ return {"Status": "Pass", "Payload": {"SearchEvents": []}}, 1, []
+
+ with mock.patch.dict(
+ vedastro_service_adapter.os.environ,
+ {
+ "VEDASTRO_API_ENDPOINT": "https://api.vedastro.org/api",
+ "VEDASTRO_ENABLE_NETWORK": "1",
+ },
+ clear=False,
+ ), mock.patch.object(
+ vedastro_service_adapter,
+ "_iter_sample_dates",
+ return_value=["2026-01-01", "2026-03-01"],
+ ), mock.patch.object(
+ vedastro_service_adapter,
+ "_post_json_with_retry",
+ side_effect=fake_post_json_with_retry,
+ ):
+ vedastro_service_adapter._run_range_scan_case( # noqa: SLF001
+ case,
+ "wealth",
+ "2026-01-01",
+ "2026-12-31",
+ )
+
+ assert captured_at_times == [
+ "12:00 01/01/2026 +08:00",
+ "12:00 01/03/2026 +08:00",
+ ]
diff --git a/tests/test_vedastro_service_adapter_executor.py b/tests/test_vedastro_service_adapter_executor.py
index 533a8283..a1be75d7 100644
--- a/tests/test_vedastro_service_adapter_executor.py
+++ b/tests/test_vedastro_service_adapter_executor.py
@@ -55,6 +55,11 @@ def test_vedastro_service_adapter_executor_schema_is_declared() -> None:
def test_vedastro_service_adapter_returns_controlled_unconfigured_status() -> None:
+ env = os.environ.copy()
+ env.pop("VEDASTRO_API_ENDPOINT", None)
+ env.pop("VEDASTRO_ENABLE_NETWORK", None)
+ env["JYOTISH_SKIP_LOCAL_ENV"] = "1"
+
completed = subprocess.run(
[sys.executable, "scripts/vedastro_service_adapter.py", "--case", "beijing_first_use_demo"],
cwd=ROOT,
@@ -62,6 +67,7 @@ def test_vedastro_service_adapter_returns_controlled_unconfigured_status() -> No
capture_output=True,
timeout=120,
check=False,
+ env=env,
)
assert completed.returncode == 0, completed.stderr or completed.stdout
@@ -76,6 +82,11 @@ def test_vedastro_service_adapter_returns_controlled_unconfigured_status() -> No
def test_ephemeris_adapter_contract_can_call_vedastro_candidate_stub() -> None:
+ env = os.environ.copy()
+ env.pop("VEDASTRO_API_ENDPOINT", None)
+ env.pop("VEDASTRO_ENABLE_NETWORK", None)
+ env["JYOTISH_SKIP_LOCAL_ENV"] = "1"
+
completed = subprocess.run(
[sys.executable, "scripts/ephemeris_adapter_contract.py", "vedastro_service_adapter_candidate"],
cwd=ROOT,
@@ -83,6 +94,7 @@ def test_ephemeris_adapter_contract_can_call_vedastro_candidate_stub() -> None:
capture_output=True,
timeout=120,
check=False,
+ env=env,
)
assert completed.returncode == 0, completed.stderr or completed.stdout
@@ -96,6 +108,8 @@ def test_ephemeris_adapter_contract_can_call_vedastro_candidate_stub() -> None:
def test_vedastro_service_adapter_builds_request_preview_before_real_network_use() -> None:
env = os.environ.copy()
env["VEDASTRO_API_ENDPOINT"] = "https://example.invalid/vedastro"
+ env.pop("VEDASTRO_ENABLE_NETWORK", None)
+ env["JYOTISH_SKIP_LOCAL_ENV"] = "1"
completed = subprocess.run(
[sys.executable, "scripts/vedastro_service_adapter.py", "--case", "beijing_first_use_demo"],
@@ -120,6 +134,8 @@ def test_vedastro_service_adapter_builds_request_preview_before_real_network_use
def test_vedastro_service_adapter_builds_range_scan_preview_before_real_network_use() -> None:
env = os.environ.copy()
env["VEDASTRO_API_ENDPOINT"] = "https://example.invalid/vedastro"
+ env.pop("VEDASTRO_ENABLE_NETWORK", None)
+ env["JYOTISH_SKIP_LOCAL_ENV"] = "1"
completed = subprocess.run(
[
@@ -160,6 +176,7 @@ def test_vedastro_range_scan_unconfigured_still_returns_official_search_events_p
env = os.environ.copy()
env.pop("VEDASTRO_API_ENDPOINT", None)
env.pop("VEDASTRO_ENABLE_NETWORK", None)
+ env["JYOTISH_SKIP_LOCAL_ENV"] = "1"
completed = subprocess.run(
[
@@ -198,6 +215,7 @@ def test_vedastro_range_scan_unconfigured_still_returns_official_search_events_p
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"
assert report["request_preview"]["official_request_profile"]["body"]["PrecisionHours"] == 100
+ assert report["request_preview"]["live_sampling_request_profile"]["body"]["AtTime"]["StdTime"] == "12:00 01/01/2026 +08:00"
def test_vedastro_schema_declares_official_search_events_live_contract() -> None:
@@ -364,6 +382,8 @@ def test_vedastro_service_adapter_can_normalize_mock_http_response() -> None:
def test_vedastro_service_adapter_can_normalize_mock_range_scan_response() -> None:
+ seen_at_times: list[str] = []
+
class Handler(BaseHTTPRequestHandler):
def do_POST(self) -> None: # noqa: N802
length = int(self.headers.get("Content-Length", "0"))
@@ -371,9 +391,7 @@ def test_vedastro_service_adapter_can_normalize_mock_range_scan_response() -> No
assert payload["Ayanamsa"] == "lahiri"
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"
- assert payload["PrecisionHours"] == 100
+ seen_at_times.append(payload["AtTime"]["StdTime"])
response = {
"events": [
{
@@ -458,6 +476,10 @@ def test_vedastro_service_adapter_can_normalize_mock_range_scan_response() -> No
assert report["evidence_ledger"][0]["score"] == 72
assert report["evidence_ledger"][0]["raw"]["name"] == "Jupiter supports marriage axis"
assert report["source_metadata"]["endpoint"].startswith("http://127.0.0.1:")
+ assert report["source_metadata"]["sampling_mode"] == "at_time_sweep"
+ assert report["source_metadata"]["sample_count"] >= 2
+ assert seen_at_times[0] == "12:00 01/01/2026 +08:00"
+ assert len(set(seen_at_times)) >= 2
def test_vedastro_range_scan_records_hashes_and_artifact_path() -> None:
@@ -536,7 +558,9 @@ def test_vedastro_range_scan_records_hashes_and_artifact_path() -> None:
assert metadata["allowlist_domain"] == "marriage"
assert metadata["allowlist_event_count"] == 1
assert metadata["filtered_event_count"] == 1
- assert metadata["attempt_count"] == 1
+ assert metadata["attempt_count"] >= 1
+ assert metadata["sample_count"] >= 1
+ assert metadata["sampling_mode"] == "at_time_sweep"
artifact_path = ROOT / metadata["artifact_path"]
assert artifact_path.exists()
artifact = json.loads(artifact_path.read_text(encoding="utf-8"))
@@ -615,8 +639,9 @@ def test_vedastro_range_scan_retries_transient_http_error() -> None:
report = json.loads(completed.stdout)
assert report["status"] == "ok"
assert report["event_count"] == 1
- assert report["source_metadata"]["attempt_count"] == 2
- assert report["source_metadata"]["retry_error_codes"] == [503]
+ assert report["source_metadata"]["attempt_count"] >= 2
+ assert 503 in report["source_metadata"]["retry_error_codes"]
+ assert report["source_metadata"]["sampling_mode"] == "at_time_sweep"
def test_vedastro_service_adapter_applies_domain_allowlist_to_range_scan_noise() -> None: