Wire VedAstro orchestrator into strict workflows

This commit is contained in:
732642856
2026-06-29 10:26:54 +08:00
parent 3452beee1b
commit 0f9b06d9d4
18 changed files with 904 additions and 162 deletions
+2
View File
@@ -18,6 +18,8 @@ jyotish-app/dist/
brain/
COVERAGE_AUDIT_REPORT.md
*.tmp
.env.local
.jyotish.local.env
.coverage
coverage.xml
htmlcov/
+11
View File
@@ -64,6 +64,17 @@ This is a **Vedic (Jyotish) astrology analysis system** designed for deep, audit
4. 如果只安装 PWA:PWA 安装壳只包装网页服务,本地 API 服务仍需单独启动;无 API 时网页会保留基础浏览器 fallback,但 PDF/高级技法需要本地 API 服务。
5. 开发者做完整自检时运行:`python3 scripts/run_quality_gate.py --frontend-click-timeout 240`。
如需把网页里的 `VedAstro Range Scan` 按钮切到真实外部响应模式,可在仓库根目录创建本机私有配置文件 `.env.local`:
```bash
VEDASTRO_API_ENDPOINT=https://api.vedastro.org/api
VEDASTRO_ENABLE_NETWORK=1
# 可选
# VEDASTRO_API_KEY=sk_live_xxx
```
该文件已被 `.gitignore` 忽略;`scripts/jyotish_api_server.py`、`scripts/vedastro_service_adapter.py`、`scripts/run_quality_gate.py` 会自动加载它。注意:当前网页按钮走的是官方 `SearchEvents` 的真实 live 采样扫描桥接,不会让 VedAstro 越权改本地 adjudicator 的 `score / dominant_label / payout_label`。
### 普通用户交付形态
| 形态 | 入口 | 命令 | 能力边界 |
+14
View File
@@ -2,6 +2,20 @@
This file is the small index for the current engineering fronts that still drive code changes.
## Shortest-Path Closure Order (2026-06-29)
To avoid scope drift, current work stays inside these four closure lanes only:
1. Relationship adjudicator closure
2. Vimsopaka + functional-role closure
3. Oracle closure batch
4. VedAstro strict ingestion
Reference plan:
- `/Users/wuyongnaren/Documents/印度占星/docs/superpowers/plans/2026-06-29-shortest-path-closure-plan.md`
Do not open new product surfaces before at least one of these four lanes is closed.
## Fragment Discipline
- Before touching strict workflow or adjudicator logic, check:
+35
View File
@@ -251,6 +251,41 @@ function buildRectificationDecisionPlan(events) {
};
}
export function buildRectificationInterviewQuestions() {
return EVENT_COLLECTION_GUIDE.map(group => {
const category = group.categories.find(key => EVENT_CATEGORIES[key]) || group.categories[0];
const cat = EVENT_CATEGORIES[category] || {};
return {
id: `guided_rectification_interview_${group.key}`,
group: group.key,
category,
varga: cat.varga || 'D1',
label_cn: group.cn,
label_en: group.en,
question_cn: `你的人生中是否发生过日期比较明确的${group.cn}?`,
question_en: `Have you had a dated ${group.en} event?`,
examples_cn: group.categories
.map(key => EVENT_CATEGORIES[key]?.cn)
.filter(Boolean)
.slice(0, 4),
};
});
}
export function rectificationInterviewAnswersToEvents(answers) {
return (answers || [])
.filter(answer => answer?.answer === 'yes' && answer.date && EVENT_CATEGORIES[answer.category])
.map(answer => {
const cat = EVENT_CATEGORIES[answer.category];
return {
date: answer.date,
category: answer.category,
desc: (answer.note || '').trim() || cat.cn,
source: 'guided_rectification_interview',
};
});
}
function buildRectificationAudit(best, results, events) {
const second = results[1] || null;
const coverage = summarizeEventCoverage(events);
+84 -1
View File
@@ -5,6 +5,7 @@
import { SIGNS, PLANET_CN, SIGN_LORDS } from './jyotish-engine.js';
import {
EVENT_CATEGORIES, EVENT_COLLECTION_GUIDE, VARGA_SENSITIVITY, runRectification,
buildRectificationInterviewQuestions, rectificationInterviewAnswersToEvents,
getHouseLord, fmtTime, dateToJD
} from './rectification-engine.js';
import { t, getLang, signName, planetName } from './i18n.js';
@@ -13,6 +14,7 @@ import { escapeHtml, escapeAttr } from './security.js';
function fmtOffset(m) { return m === 0 ? t('rect.baseline') : `${m > 0 ? '+' : ''}${m}min`; }
let rectEvents = [];
let rectInterviewAnswers = {};
export function renderRectificationTab(container) {
const lang = getLang();
@@ -25,6 +27,8 @@ export function renderRectificationTab(container) {
<div class="rect-config card">
<h4 class="sub-title">${t('rect.config')}</h4>
<div class="rect-config-row">
<div class="form-group"><label>出生时间区间开始</label><input type="time" id="rect-window-start" step="60"></div>
<div class="form-group"><label>出生时间区间结束</label><input type="time" id="rect-window-end" step="60"></div>
<div class="form-group"><label>${t('rect.range')}</label>
<select id="rect-range">
<option value="5">${t('rect.range.5')}</option>
@@ -51,6 +55,22 @@ export function renderRectificationTab(container) {
</table>
</details>
</div>
<div class="rect-interview card">
<div class="rect-interview-head">
<div>
<h4 class="sub-title">快速事件访谈</h4>
<p>只回答是/否;选“是”时补一个大概日期,系统会自动转成生命事件。</p>
</div>
<span>guided_rectification_interview</span>
</div>
<div class="rect-interview-list">
${buildRectificationInterviewQuestions().map(question => renderInterviewQuestion(question, lang)).join('')}
</div>
<div class="rect-interview-actions">
<button type="button" class="rect-secondary-btn" id="rect-import-interview">把“是”的回答加入事件</button>
<span id="rect-interview-status" aria-live="polite"></span>
</div>
</div>
<div class="rect-events card">
<h4 class="sub-title">${t('rect.events')} <span class="rect-count" id="rect-event-count">0 ${t('rect.event.count')}</span></h4>
<p class="rect-hint">${t('rect.events.hint')}</p>
@@ -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 `<div class="rect-interview-item" data-question-id="${escapeAttr(question.id)}" data-category="${escapeAttr(question.category)}">
<div class="rect-interview-copy">
<strong>${escapeHtml(prompt)}</strong>
<span>${escapeHtml(label)} · ${escapeHtml(question.varga)}${examples ? ` · ${escapeHtml(examples)}` : ''}</span>
</div>
<div class="rect-answer-group" role="group" aria-label="${escapeAttr(prompt)}">
<button type="button" data-rect-answer="yes">是</button>
<button type="button" data-rect-answer="no">否</button>
<button type="button" data-rect-answer="other">其他</button>
</div>
<div class="rect-answer-detail hidden">
<input type="date" class="rect-answer-date" aria-label="事件日期">
<input type="text" class="rect-answer-note" placeholder="补充说明,可选">
</div>
</div>`;
}
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 = {}; }
+91
View File
@@ -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; }
+22 -23
View File
@@ -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):
+11
View File
@@ -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。
+3 -86
View File
@@ -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
+4
View File
@@ -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
+133
View File
@@ -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,
},
}
+189 -45
View File
@@ -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]:
+12
View File
@@ -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/*`、现有测试与产品差距矩阵。
@@ -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"] = {
@@ -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
@@ -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"]
+72
View File
@@ -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",
]
@@ -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: