Wire VedAstro orchestrator into strict workflows
This commit is contained in:
@@ -18,6 +18,8 @@ jyotish-app/dist/
|
||||
brain/
|
||||
COVERAGE_AUDIT_REPORT.md
|
||||
*.tmp
|
||||
.env.local
|
||||
.jyotish.local.env
|
||||
.coverage
|
||||
coverage.xml
|
||||
htmlcov/
|
||||
|
||||
@@ -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`。
|
||||
|
||||
### 普通用户交付形态
|
||||
|
||||
| 形态 | 入口 | 命令 | 能力边界 |
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 = {}; }
|
||||
|
||||
@@ -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
@@ -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
@@ -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。
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
@@ -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]:
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user