Productize VedAstro user range scan entry
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
# VedAstro User Range Scan Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Expose VedAstro range scan as an ordinary user action in the web app.
|
||||
|
||||
**Architecture:** Reuse the existing `vedastro_service_adapter` as the only network boundary. The API validates user chart/date/domain inputs and returns the adapter result shape. The frontend displays blocked/live results in Trust Center and attaches the latest result to `chartData.modules.vedastro_range_scan_result`.
|
||||
|
||||
**Tech Stack:** Python stdlib HTTP server, existing JS API bridge, existing Trust Center/provenance UI.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Default CI must not require network.
|
||||
- Do not leak full VedAstro endpoint paths or API keys to the frontend.
|
||||
- VedAstro evidence remains secondary external timing evidence.
|
||||
- Use TDD: failing test first, then minimal implementation.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: API Route
|
||||
|
||||
**Files:**
|
||||
- Modify: `scripts/vedastro_service_adapter.py`
|
||||
- Modify: `scripts/jyotish_api_server.py`
|
||||
- Test: `tests/test_api_server_security.py`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `vedastro_service_adapter.run_range_scan_for_case(case, domain, start_date, end_date, case_id='user_chart')`
|
||||
- Produces: `POST /api/vedastro/range_scan`
|
||||
|
||||
- [ ] Write failing route test.
|
||||
- [ ] Add adapter helper for user birth case.
|
||||
- [ ] Add API route and validation.
|
||||
- [ ] Run focused API test.
|
||||
|
||||
### Task 2: Frontend User Panel
|
||||
|
||||
**Files:**
|
||||
- Modify: `jyotish-app/api-bridge.js`
|
||||
- Modify: `jyotish-app/public/api-bridge.js`
|
||||
- Modify: `jyotish-app/main.js`
|
||||
- Test: `tests/test_frontend_productization.py`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `window.JyotishAPI.runVedAstroRangeScan(payload)`
|
||||
- Produces: `renderVedAstroUserScanPanel`, `runVedAstroRangeScanFromPanel`
|
||||
|
||||
- [ ] Write failing static frontend test.
|
||||
- [ ] Add bridge function.
|
||||
- [ ] Add Trust Center scan panel and action handler.
|
||||
- [ ] Attach latest result to `chartData.modules.vedastro_range_scan_result`.
|
||||
- [ ] Run focused frontend test and build.
|
||||
|
||||
### Task 3: Verification And Records
|
||||
|
||||
**Files:**
|
||||
- Modify: `task_plan.md`
|
||||
- Modify: `progress.md`
|
||||
- Modify: `findings.md`
|
||||
|
||||
- [ ] Run VedAstro/API/frontend focused tests.
|
||||
- [ ] Run quick quality gate.
|
||||
- [ ] Update project records with product boundary.
|
||||
@@ -232,6 +232,10 @@ async function getVedAstroStatus() {
|
||||
return fetchJson('/api/vedastro/status');
|
||||
}
|
||||
|
||||
async function runVedAstroRangeScan(payload) {
|
||||
return postJson('/api/vedastro/range_scan', payload);
|
||||
}
|
||||
|
||||
async function getTechniqueCatalog() {
|
||||
let lastError = null;
|
||||
for (const base of getApiBases(true)) {
|
||||
@@ -455,6 +459,7 @@ window.JyotishAPI = {
|
||||
getAPIHealth,
|
||||
getCapabilityAudit,
|
||||
getVedAstroStatus,
|
||||
runVedAstroRangeScan,
|
||||
getTechniqueCatalog,
|
||||
runTechniqueExample,
|
||||
computeAnnual,
|
||||
|
||||
@@ -1806,6 +1806,7 @@ function renderTrustCenterPanel() {
|
||||
${renderVedAstroStatus(vedastro)}
|
||||
</div>
|
||||
${renderRuntimeHealthPanel(runtime)}
|
||||
${renderVedAstroUserScanPanel()}
|
||||
${renderValidationTransparencyPanel()}
|
||||
${renderDashaShadbalaCalibrationPanel()}
|
||||
${renderOracleEvidenceIntakePanel()}
|
||||
@@ -2270,6 +2271,84 @@ function renderVedAstroStatus(status = getVedAstroStatus()) {
|
||||
return renderTrustStatus('VedAstro 外部雷达', status.label, status.note);
|
||||
}
|
||||
|
||||
function getVedAstroScanState() {
|
||||
return window.__jyotishVedAstroRangeScan || { status: 'idle' };
|
||||
}
|
||||
|
||||
function renderVedAstroUserScanPanel(state = getVedAstroScanState()) {
|
||||
const today = new Date();
|
||||
const start = state.start_date || `${today.getFullYear()}-01-01`;
|
||||
const end = state.end_date || `${today.getFullYear()}-12-31`;
|
||||
const domain = state.domain || 'career';
|
||||
return `
|
||||
<div class="runtime-health-panel vedastro-user-scan-panel" data-vedastro-user-scan="true">
|
||||
<div class="calculation-settings-head">
|
||||
<strong>VedAstro Range Scan</strong>
|
||||
<span>对当前星盘运行外部高频事件雷达;外部证据只进 secondary context。</span>
|
||||
</div>
|
||||
<div class="calculation-settings-grid">
|
||||
<label>
|
||||
<span>领域</span>
|
||||
<select id="vedastro-scan-domain">
|
||||
<option value="career"${domain === 'career' ? ' selected' : ''}>事业</option>
|
||||
<option value="relationship"${domain === 'relationship' ? ' selected' : ''}>婚恋</option>
|
||||
<option value="finance"${domain === 'finance' ? ' selected' : ''}>财富</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>开始</span>
|
||||
<input id="vedastro-scan-start" type="date" value="${escapeAttr(start)}">
|
||||
</label>
|
||||
<label>
|
||||
<span>结束</span>
|
||||
<input id="vedastro-scan-end" type="date" value="${escapeAttr(end)}">
|
||||
</label>
|
||||
</div>
|
||||
<div class="provenance-actions">
|
||||
<button type="button" class="provenance-action" data-action="vedastro-run-range-scan">运行 VedAstro 外部雷达扫描</button>
|
||||
</div>
|
||||
${renderVedAstroRangeScanResult(state)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderVedAstroRangeScanResult(state = getVedAstroScanState()) {
|
||||
if (state.status === 'running') {
|
||||
return '<div class="workspace-import-status" aria-live="polite">正在请求 /api/vedastro/range_scan...</div>';
|
||||
}
|
||||
if (state.status === 'idle') {
|
||||
return '<div class="calculation-settings-note">请先生成星盘,再选择年份或日期范围运行扫描。没有配置 VEDASTRO_API_ENDPOINT 时会返回 blocked 边界。</div>';
|
||||
}
|
||||
if (state.status === 'error') {
|
||||
return `<div class="workspace-import-status" aria-live="polite">VedAstro Range Scan 未完成:${escapeHtml(state.error || '本地 API 不可用')}</div>`;
|
||||
}
|
||||
const result = state.result?.result || state.result || {};
|
||||
const metadata = result.source_metadata || {};
|
||||
const events = Array.isArray(result.evidence_ledger) ? result.evidence_ledger.slice(0, 5) : [];
|
||||
const status = result.status || state.status || '-';
|
||||
const eventCount = result.event_count ?? events.length ?? 0;
|
||||
return `
|
||||
<div class="workspace-import-status" aria-live="polite">
|
||||
VedAstro Range Scan:${escapeHtml(status)} · ${escapeHtml(String(eventCount))} events · ${escapeHtml(state.start_date || '-')}/${escapeHtml(state.end_date || '-')}
|
||||
</div>
|
||||
<div class="calculation-settings-note">
|
||||
${escapeHtml(result.reason || state.result?.boundary || '外部雷达结果已挂到 chartData.modules.vedastro_range_scan_result;本地 Jyotish gates 仍为主判断。')}
|
||||
${metadata.artifact_path ? ` artifact: ${escapeHtml(metadata.artifact_path)}` : ''}
|
||||
</div>
|
||||
${events.length ? `
|
||||
<div class="runtime-health-grid">
|
||||
${events.map(event => `
|
||||
<div class="trust-status-card">
|
||||
<span>${escapeHtml(event.signal_label || event.event_id || 'VedAstro event')}</span>
|
||||
<strong>${escapeHtml(event.start || '-')}</strong>
|
||||
<small>${escapeHtml(event.signal_family || event.domain || 'range_scan')}</small>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
` : ''}
|
||||
`;
|
||||
}
|
||||
|
||||
function getRuntimeHealthStatus() {
|
||||
const health = window.__jyotishRuntimeHealth;
|
||||
if (!health) {
|
||||
@@ -2548,6 +2627,69 @@ async function runTrustCenterRealCaseRevalidation() {
|
||||
renderAll();
|
||||
}
|
||||
|
||||
function buildVedAstroRangeScanPayload(panel) {
|
||||
const birth = window.__jyotishBirth || normalizeSavedBirth(chartData || {});
|
||||
if (!chartData && !window.__jyotishBirth) {
|
||||
throw new Error('请先生成星盘,再运行 VedAstro 外部雷达扫描。');
|
||||
}
|
||||
const settings = readCalculationSettings();
|
||||
return {
|
||||
...birth,
|
||||
domain: panel.querySelector('#vedastro-scan-domain')?.value || 'career',
|
||||
start_date: panel.querySelector('#vedastro-scan-start')?.value || `${new Date().getFullYear()}-01-01`,
|
||||
end_date: panel.querySelector('#vedastro-scan-end')?.value || `${new Date().getFullYear()}-12-31`,
|
||||
ayanamsa_policy: settings.ayanamsa,
|
||||
node_policy: settings.nodeMode,
|
||||
case_id: getCurrentChartId(chartData || { birth_info: birth }) || 'user_chart',
|
||||
};
|
||||
}
|
||||
|
||||
async function runVedAstroRangeScanFromPanel(panel) {
|
||||
const status = $('trust-center-status');
|
||||
let payload;
|
||||
try {
|
||||
payload = buildVedAstroRangeScanPayload(panel);
|
||||
} catch (error) {
|
||||
window.__jyotishVedAstroRangeScan = { status: 'error', error: error?.message || '缺少当前星盘' };
|
||||
if (status) status.textContent = window.__jyotishVedAstroRangeScan.error;
|
||||
renderAll();
|
||||
return;
|
||||
}
|
||||
window.__jyotishVedAstroRangeScan = {
|
||||
status: 'running',
|
||||
domain: payload.domain,
|
||||
start_date: payload.start_date,
|
||||
end_date: payload.end_date,
|
||||
};
|
||||
if (status) status.textContent = '正在运行 VedAstro 外部雷达扫描...';
|
||||
renderAll();
|
||||
try {
|
||||
const result = await window.JyotishAPI.runVedAstroRangeScan(payload);
|
||||
window.__jyotishVedAstroRangeScan = {
|
||||
status: result.result?.status || 'ok',
|
||||
domain: payload.domain,
|
||||
start_date: payload.start_date,
|
||||
end_date: payload.end_date,
|
||||
result,
|
||||
};
|
||||
if (chartData) {
|
||||
chartData.modules = chartData.modules || {};
|
||||
chartData.modules.vedastro_range_scan_result = result.result;
|
||||
}
|
||||
if (status) status.textContent = `VedAstro Range Scan 完成:${result.result?.status || 'ok'}`;
|
||||
} catch (error) {
|
||||
window.__jyotishVedAstroRangeScan = {
|
||||
status: 'error',
|
||||
domain: payload.domain,
|
||||
start_date: payload.start_date,
|
||||
end_date: payload.end_date,
|
||||
error: error?.message || 'VedAstro Range Scan failed',
|
||||
};
|
||||
if (status) status.textContent = `VedAstro Range Scan 未完成:${window.__jyotishVedAstroRangeScan.error}`;
|
||||
}
|
||||
renderAll();
|
||||
}
|
||||
|
||||
async function promptPWAInstall() {
|
||||
const status = $('trust-center-status');
|
||||
const prompt = window.__jyotishDeferredInstallPrompt;
|
||||
@@ -3098,6 +3240,9 @@ function bindProvenanceActions() {
|
||||
if (btn.dataset.action === 'trust-run-health') {
|
||||
runTrustCenterHealthCheck();
|
||||
}
|
||||
if (btn.dataset.action === 'vedastro-run-range-scan') {
|
||||
runVedAstroRangeScanFromPanel(panel);
|
||||
}
|
||||
if (btn.dataset.action === 'pwa-install') {
|
||||
promptPWAInstall();
|
||||
}
|
||||
|
||||
@@ -232,6 +232,10 @@ async function getVedAstroStatus() {
|
||||
return fetchJson('/api/vedastro/status');
|
||||
}
|
||||
|
||||
async function runVedAstroRangeScan(payload) {
|
||||
return postJson('/api/vedastro/range_scan', payload);
|
||||
}
|
||||
|
||||
async function getTechniqueCatalog() {
|
||||
let lastError = null;
|
||||
for (const base of getApiBases(true)) {
|
||||
@@ -455,6 +459,7 @@ window.JyotishAPI = {
|
||||
getAPIHealth,
|
||||
getCapabilityAudit,
|
||||
getVedAstroStatus,
|
||||
runVedAstroRangeScan,
|
||||
getTechniqueCatalog,
|
||||
runTechniqueExample,
|
||||
computeAnnual,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"scope": "pyjhora_oracle_artifact_manifest",
|
||||
"generated_at": "2026-06-28T07:23:17.173699+00:00",
|
||||
"generated_at": "2026-06-28T23:44:26.808999+00:00",
|
||||
"artifact_count": 8,
|
||||
"packet_count": 8,
|
||||
"artifacts": [
|
||||
|
||||
@@ -305,6 +305,9 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
|
||||
elif path == '/api/relationship':
|
||||
result = self._compute_relationship(body)
|
||||
self._json(result)
|
||||
elif path == '/api/vedastro/range_scan':
|
||||
result = self._compute_vedastro_range_scan(body)
|
||||
self._json(result)
|
||||
elif path == '/api/import_chart':
|
||||
result = self._import_chart_text(body)
|
||||
self._json(result)
|
||||
@@ -596,6 +599,73 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
|
||||
with open(path, 'rb') as fh:
|
||||
return base64.b64encode(fh.read()).decode('ascii')
|
||||
|
||||
def _compute_vedastro_range_scan(self, body):
|
||||
ui_domain = str(body.get('domain') or 'career').strip().lower()
|
||||
domain_map = {
|
||||
'career': 'career',
|
||||
'relationship': 'marriage',
|
||||
'marriage': 'marriage',
|
||||
'finance': 'wealth',
|
||||
'wealth': 'wealth',
|
||||
}
|
||||
if ui_domain not in domain_map:
|
||||
raise BadRequest('domain must be career, relationship, marriage, finance, or wealth')
|
||||
start_date = str(body.get('start_date') or '').strip()
|
||||
end_date = str(body.get('end_date') or '').strip()
|
||||
if not start_date or not end_date:
|
||||
raise BadRequest('start_date and end_date are required')
|
||||
try:
|
||||
start_dt = datetime.strptime(start_date, '%Y-%m-%d')
|
||||
end_dt = datetime.strptime(end_date, '%Y-%m-%d')
|
||||
except ValueError as e:
|
||||
raise BadRequest('start_date and end_date must be YYYY-MM-DD') from e
|
||||
if end_dt < start_dt:
|
||||
raise BadRequest('end_date must be on or after start_date')
|
||||
|
||||
year = self._get_int(body, 'year', None, 1800, 2400)
|
||||
month = self._get_int(body, 'month', None, 1, 12)
|
||||
day = self._get_int(body, 'day', None, 1, 31)
|
||||
hour = self._get_float(body, 'hour', 12, 0, 23)
|
||||
minute = self._get_float(body, 'minute', 0, 0, 59)
|
||||
second = self._get_birth_second(body)
|
||||
lat = self._get_float(body, 'lat', 0, -90, 90)
|
||||
lon = self._get_float(body, 'lon', 0, -180, 180)
|
||||
tz = self._parse_timezone(body, lat, lon, year, month, day, hour, minute, second)
|
||||
try:
|
||||
datetime(year, month, day, int(hour), int(minute), int(second))
|
||||
except ValueError as e:
|
||||
raise BadRequest('Invalid birth date') from e
|
||||
|
||||
adapter_domain = domain_map[ui_domain]
|
||||
case = {
|
||||
'year': year,
|
||||
'month': month,
|
||||
'day': day,
|
||||
'hour': hour,
|
||||
'minute': minute,
|
||||
'second': second,
|
||||
'lat': lat,
|
||||
'lon': lon,
|
||||
'tz': tz,
|
||||
'ayanamsa_policy': body.get('ayanamsa_policy') or body.get('ayanamsa') or 'lahiri',
|
||||
'node_policy': body.get('node_policy') or body.get('node_mode') or 'mean',
|
||||
}
|
||||
result = _load_local_module('vedastro_service_adapter').run_range_scan_for_case(
|
||||
case,
|
||||
adapter_domain,
|
||||
start_date,
|
||||
end_date,
|
||||
case_id=str(body.get('case_id') or 'user_chart'),
|
||||
)
|
||||
return {
|
||||
'success': True,
|
||||
'endpoint': 'vedastro_range_scan',
|
||||
'ui_domain': ui_domain,
|
||||
'adapter_domain': adapter_domain,
|
||||
'result': result,
|
||||
'boundary': 'VedAstro range scan is optional external timing evidence; local Jyotish gates remain authoritative.',
|
||||
}
|
||||
|
||||
def _compute_oracle_evidence(self, body):
|
||||
packet = body.get('packet')
|
||||
if not isinstance(packet, dict):
|
||||
|
||||
@@ -80,9 +80,9 @@ OFFICIAL_SEARCH_EVENTS_ENDPOINT_PATH = "/Calculate/SearchEvents"
|
||||
OFFICIAL_SEARCH_EVENTS_METHOD = "POST"
|
||||
OFFICIAL_SEARCH_EVENTS_PROFILE_VERSION = "official_builder_search_events_v1"
|
||||
OFFICIAL_RANGE_SCAN_EVENT_TAGS = {
|
||||
"marriage": ["Marriage", "General"],
|
||||
"wealth": ["LendingMoney", "BorrowingMoney", "General"],
|
||||
"career": ["General", "Building", "Travel"],
|
||||
"marriage": ["Marriage", "Personal", "General"],
|
||||
"wealth": ["LendingMoney", "BorrowingMoney", "BuyingSelling", "General"],
|
||||
"career": ["Personal", "General", "Building", "Travel"],
|
||||
}
|
||||
VEDASTRO_CALCULATION_COVERAGE = {
|
||||
"official_python_library_calculations": "596+",
|
||||
@@ -204,6 +204,52 @@ RANGE_SCAN_SIGNAL_METADATA = {
|
||||
},
|
||||
},
|
||||
}
|
||||
RANGE_SCAN_OFFICIAL_TAG_MATCHES = {
|
||||
"marriage": {"Marriage"},
|
||||
"wealth": {"LendingMoney", "BorrowingMoney", "BuyingSelling"},
|
||||
"career": {"Building", "Travel"},
|
||||
}
|
||||
RANGE_SCAN_ALIAS_TERMS = {
|
||||
"marriage": {
|
||||
"marriage",
|
||||
"spouse",
|
||||
"wedding",
|
||||
"relationship",
|
||||
"partner",
|
||||
"partnership",
|
||||
},
|
||||
"wealth": {
|
||||
"wealth",
|
||||
"money",
|
||||
"finance",
|
||||
"financial",
|
||||
"income",
|
||||
"gain",
|
||||
"gains",
|
||||
"lending",
|
||||
"borrowing",
|
||||
"business",
|
||||
"cash",
|
||||
},
|
||||
"career": {
|
||||
"career",
|
||||
"profession",
|
||||
"work",
|
||||
"job",
|
||||
"business",
|
||||
"travel",
|
||||
"building",
|
||||
"public",
|
||||
"status",
|
||||
},
|
||||
}
|
||||
MATCH_METADATA_BY_TYPE = {
|
||||
"exact_id": {"signal_lift": 3, "confidence": "high"},
|
||||
"official_tag": {"signal_lift": 2, "confidence": "medium_high"},
|
||||
"alias": {"signal_lift": 1, "confidence": "low"},
|
||||
"rejected": {"signal_lift": 0, "confidence": "rejected"},
|
||||
}
|
||||
ALIAS_NEGATIVE_GUARD_TERMS = {"noise", "without", "generic", "irrelevant", "insignificance", "not"}
|
||||
DEFAULT_TIMEOUT_SECONDS = 120
|
||||
TIMEOUT_ENV = "VEDASTRO_TIMEOUT_SECONDS"
|
||||
BACKOFF_ENV = "VEDASTRO_RETRY_BACKOFF_SECONDS"
|
||||
@@ -653,9 +699,20 @@ def _normalize_range_scan_success(
|
||||
allowlist = RANGE_SCAN_EVENT_ALLOWLIST.get(domain, {})
|
||||
allowed_ids = allowlist.get("event_ids", set())
|
||||
allowed_tags = allowlist.get("tags", set())
|
||||
official_tags = RANGE_SCAN_OFFICIAL_TAG_MATCHES.get(domain, set())
|
||||
alias_terms = RANGE_SCAN_ALIAS_TERMS.get(domain, set())
|
||||
|
||||
original_event_count = len(events)
|
||||
evidence_ledger = []
|
||||
mapping_details = []
|
||||
matched_tags: set[str] = set()
|
||||
recommended_allowlist_candidates: set[str] = set()
|
||||
match_counts = {
|
||||
"exact_id": 0,
|
||||
"official_tag": 0,
|
||||
"alias": 0,
|
||||
"rejected": 0,
|
||||
}
|
||||
for index, event in enumerate(events, start=1):
|
||||
if not isinstance(event, dict):
|
||||
continue
|
||||
@@ -665,15 +722,75 @@ def _normalize_range_scan_success(
|
||||
if not isinstance(tags, list):
|
||||
tags = []
|
||||
tag_set = {str(tag) for tag in tags}
|
||||
if event_id not in allowed_ids and tag_set.isdisjoint(allowed_tags):
|
||||
continue
|
||||
matched_by = "rejected"
|
||||
matched_terms: list[str] = []
|
||||
drop_reason = "no_supported_match"
|
||||
signal_metadata = RANGE_SCAN_SIGNAL_METADATA.get(domain, {}).get(event_id, {})
|
||||
if event_id in allowed_ids:
|
||||
matched_by = "exact_id"
|
||||
matched_terms = [event_id]
|
||||
drop_reason = ""
|
||||
else:
|
||||
official_tag_hits = sorted(tag_set.intersection(official_tags))
|
||||
if official_tag_hits:
|
||||
matched_by = "official_tag"
|
||||
matched_terms = official_tag_hits
|
||||
drop_reason = ""
|
||||
else:
|
||||
haystack_parts = [
|
||||
str(event_id),
|
||||
str(event.get("Description") or ""),
|
||||
str(event.get("description") or ""),
|
||||
str(event.get("Name") or ""),
|
||||
" ".join(str(tag) for tag in tags),
|
||||
]
|
||||
haystack = " ".join(part.lower() for part in haystack_parts if part)
|
||||
alias_hits = sorted(term for term in alias_terms if term in haystack)
|
||||
guard_hits = sorted(term for term in ALIAS_NEGATIVE_GUARD_TERMS if term in haystack)
|
||||
if alias_hits and not guard_hits:
|
||||
matched_by = "alias"
|
||||
matched_terms = alias_hits
|
||||
drop_reason = ""
|
||||
match_counts[matched_by] += 1
|
||||
if matched_by == "rejected":
|
||||
mapping_details.append(
|
||||
{
|
||||
"event_id": event_id,
|
||||
"matched_by": matched_by,
|
||||
"matched_terms": matched_terms,
|
||||
"drop_reason": drop_reason,
|
||||
"tags": tags,
|
||||
}
|
||||
)
|
||||
continue
|
||||
if matched_by == "official_tag":
|
||||
matched_tags.update(matched_terms)
|
||||
if event_id not in allowed_ids and not tag_set.intersection(allowed_tags):
|
||||
recommended_allowlist_candidates.add(event_id)
|
||||
elif matched_by == "alias":
|
||||
if event_id not in allowed_ids:
|
||||
recommended_allowlist_candidates.add(event_id)
|
||||
match_meta = MATCH_METADATA_BY_TYPE[matched_by]
|
||||
mapping_details.append(
|
||||
{
|
||||
"event_id": event_id,
|
||||
"matched_by": matched_by,
|
||||
"matched_terms": matched_terms,
|
||||
"drop_reason": drop_reason,
|
||||
"tags": tags,
|
||||
}
|
||||
)
|
||||
evidence_ledger.append(
|
||||
{
|
||||
"source": "vedastro_service_adapter_candidate",
|
||||
"operation": "range_scan",
|
||||
"domain": domain,
|
||||
"event_id": event_id,
|
||||
"matched_by": matched_by,
|
||||
"matched_terms": matched_terms,
|
||||
"signal_lift": match_meta["signal_lift"],
|
||||
"confidence": match_meta["confidence"],
|
||||
"drop_reason": None,
|
||||
"signal_key": signal_metadata.get("signal_key"),
|
||||
"signal_label": signal_metadata.get("signal_label") or event.get("name") or event_id,
|
||||
"signal_family": signal_metadata.get("signal_family"),
|
||||
@@ -709,6 +826,15 @@ def _normalize_range_scan_success(
|
||||
"allowlist_event_count": len(evidence_ledger),
|
||||
"filtered_event_count": len(evidence_ledger),
|
||||
"raw_event_count": original_event_count,
|
||||
"mapping_replay": {
|
||||
"raw_event_count": original_event_count,
|
||||
"filtered_event_count": len(evidence_ledger),
|
||||
"zero_event_domains": [domain] if original_event_count > 0 and not evidence_ledger else [],
|
||||
"match_counts": match_counts,
|
||||
"matched_tags": sorted(matched_tags),
|
||||
"recommended_allowlist_candidates": sorted(recommended_allowlist_candidates),
|
||||
"events": mapping_details,
|
||||
},
|
||||
**(payload.get("source_metadata") or {}),
|
||||
}
|
||||
result = {
|
||||
|
||||
@@ -108,6 +108,9 @@
|
||||
- [x] 公开演示环境 polish:首屏与 Trust Center 新增静态 demo/PWA 无 API 能力边界,README 与 `deployment_preflight.py` 增加 `static_demo_boundary_visible` 守门,明确 Vercel/Netlify/GitHub Pages 只适合作为静态壳,完整技法走 Docker Compose 或本地双服务。
|
||||
- [x] Dasha/Shadbala 外部 oracle 边界第一步:新增 `references/oracle/dasha_shadbala_oracle_cases.json` 与 `scripts/oracle_boundary_audit.py`,把用户 PDF 的 Vimshottari 起点差异和 Shadbala 分量级校准缺口纳入可重复审计报告。
|
||||
- [x] VedAstro 黄经 oracle 接入:`longitude_cases` 已记录用户盘 9 项外部 sidereal longitude,本地最大差约 26.23 角秒且 D1/D9 落点一致;该样本只用于 ephemeris drift 审计,不作为 Dasha/Shadbala 调参依据。
|
||||
- [x] VedAstro Adapter MVP 方案 A:`vedastro_service_adapter` 已补来源哈希、响应哈希、调用时间、endpoint host、artifact path、重试元数据与本地 evidence artifact;`/api/vedastro/status`、Trust Center、`vedastro-live` 质量门和 MCP strict workflow 直接消费 `modules.vedastro_range_scan_result` 已接入。
|
||||
- [x] VedAstro 普通用户入口:新增 `/api/vedastro/range_scan` 与 Trust Center `VedAstro Range Scan` 面板,用户生成星盘后可选择事业/婚恋/财富和日期范围运行外部雷达;未配置 endpoint 时返回受控 blocked,配置后走同一 adapter 实网链路。
|
||||
- [ ] VedAstro 官方实网 endpoint smoke:等待配置 `VEDASTRO_API_ENDPOINT` 与 `VEDASTRO_ENABLE_NETWORK=1` 后运行 `python3 scripts/run_quality_gate.py --profile vedastro-live`,当前默认 CI 只验证受控 `blocked` 边界,不声称官方实网已闭环。
|
||||
- [x] Multi-Ayanamsa 计算层可验证切换:`full-reading --ayanamsa` 已在输出中记录 `ayanamsa_name/display/value`,`compute_chart_data(..., ayanamsa_name=...)` 也能直接切换;测试覆盖 Lahiri/Raman/KP 差异。
|
||||
- [x] AI Native Prompt/RAG 承载层第一步:`full-reading.ai_prompt_pack` 输出证据快照、检索文档、边界约束和结构化中文提示词,供网页/app 或 skill 后端 AI 代理生成高阶解读。
|
||||
- [x] Antigravity AI 副手工作单:新增 `docs/research/antigravity_sidecar_work_order_2026_06_25.md`,把 Antigravity 限定为外部 oracle 样本采集、网页/app 审计、skill 同步审计和浏览器用户流验证,避免与核心计算修改冲突。
|
||||
|
||||
@@ -108,6 +108,30 @@ class _VedAstroStatusCaptureHandler(JyotishAPIHandler):
|
||||
return json.loads(self.wfile.getvalue().decode('utf-8'))
|
||||
|
||||
|
||||
class _PostCaptureHandler(JyotishAPIHandler):
|
||||
def __init__(self, path: str, payload: dict) -> None:
|
||||
raw = json.dumps(payload).encode('utf-8')
|
||||
self.headers = _FakeHeaders({'Content-Length': str(len(raw))})
|
||||
self.server = _FakeServer()
|
||||
self.path = path
|
||||
self.rfile = BytesIO(raw)
|
||||
self.wfile = BytesIO()
|
||||
self.status_code = None
|
||||
self.response_headers = []
|
||||
|
||||
def send_response(self, code, message=None): # noqa: ANN001
|
||||
self.status_code = code
|
||||
|
||||
def send_header(self, key, value): # noqa: ANN001
|
||||
self.response_headers.append((key, value))
|
||||
|
||||
def end_headers(self):
|
||||
return None
|
||||
|
||||
def payload(self) -> dict:
|
||||
return json.loads(self.wfile.getvalue().decode('utf-8'))
|
||||
|
||||
|
||||
def test_default_cors_origins_are_local_only() -> None:
|
||||
assert 'http://localhost:3456' in DEFAULT_ALLOWED_ORIGINS
|
||||
assert '*' not in DEFAULT_ALLOWED_ORIGINS
|
||||
@@ -166,6 +190,42 @@ def test_vedastro_status_endpoint_exposes_safe_adapter_state(monkeypatch) -> Non
|
||||
assert payload['live_profile'] == 'vedastro-live'
|
||||
|
||||
|
||||
def test_vedastro_range_scan_endpoint_uses_user_birth_and_returns_controlled_blocked_state(monkeypatch) -> None:
|
||||
monkeypatch.delenv('VEDASTRO_API_ENDPOINT', raising=False)
|
||||
monkeypatch.delenv('VEDASTRO_ENABLE_NETWORK', raising=False)
|
||||
handler = _PostCaptureHandler('/api/vedastro/range_scan', {
|
||||
'domain': 'relationship',
|
||||
'start_date': '2026-01-01',
|
||||
'end_date': '2026-12-31',
|
||||
'year': REDACTED_YEAR,
|
||||
'month': 4,
|
||||
'day': 17,
|
||||
'hour': 14,
|
||||
'minute': 49,
|
||||
'second': 0,
|
||||
'lat': 36.4467,
|
||||
'lon': 114.2,
|
||||
'tz': 8,
|
||||
'ayanamsa_policy': 'lahiri',
|
||||
'node_policy': 'mean',
|
||||
})
|
||||
|
||||
handler.do_POST()
|
||||
|
||||
assert handler.status_code == 200
|
||||
payload = handler.payload()
|
||||
assert payload['success'] is True
|
||||
assert payload['endpoint'] == 'vedastro_range_scan'
|
||||
assert payload['ui_domain'] == 'relationship'
|
||||
assert payload['adapter_domain'] == 'marriage'
|
||||
assert payload['result']['status'] == 'service_endpoint_not_configured'
|
||||
assert payload['result']['operation'] == 'range_scan'
|
||||
assert payload['result']['request_preview']['year'] == REDACTED_YEAR
|
||||
assert payload['result']['request_preview']['lat'] == 36.4467
|
||||
assert payload['result']['request_preview']['domain'] == 'marriage'
|
||||
assert payload['boundary'] == 'VedAstro range scan is optional external timing evidence; local Jyotish gates remain authoritative.'
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
('key', 'value', 'minimum', 'maximum'),
|
||||
[
|
||||
@@ -518,6 +578,124 @@ def test_report_artifact_can_render_functional_benefic_malefic_summary() -> None
|
||||
assert '高严谨模式下必须叠加功能性吉凶星。' in html
|
||||
|
||||
|
||||
def test_report_artifact_can_render_relationship_strict_narrative_summary() -> None:
|
||||
handler = _handler()
|
||||
result = handler._compute_report_artifact({
|
||||
'format': 'html',
|
||||
'name': 'relationship-strict-report',
|
||||
'html': '<!doctype html><html><body><h1>Jyotish</h1></body></html>',
|
||||
'relationship_narrative': {
|
||||
'headline': '婚恋严格裁决已接入 synastry taxonomy,可把合盘支持翻译成次级关系语义。',
|
||||
'strengths': ['合盘支持已进入婚恋主链,但它只说明关系兼容度有帮助。'],
|
||||
'risks': ['当前 confidence cap 偏低,dual dasha / external timing / marriage convergence 存在冲突或不足。'],
|
||||
'boundaries': ['婚恋高严谨模式至少需要 D1、D9、UL、Vimshottari 与 Narayana dual dasha 同时在场。'],
|
||||
},
|
||||
})
|
||||
|
||||
assert result['success'] is True
|
||||
html = Path(result['html_path']).read_text(encoding='utf-8')
|
||||
assert 'Relationship Strict Narrative' in html
|
||||
assert 'synastry taxonomy' in html
|
||||
assert 'dual dasha' in html
|
||||
assert 'D1、D9、UL' in html
|
||||
|
||||
|
||||
def test_report_artifact_relationship_strict_narrative_keeps_conflict_downgrade_language() -> None:
|
||||
handler = _handler()
|
||||
result = handler._compute_report_artifact({
|
||||
'format': 'html',
|
||||
'name': 'relationship-strict-conflict-report',
|
||||
'html': '<!doctype html><html><body><h1>Jyotish</h1></body></html>',
|
||||
'relationship_narrative': {
|
||||
'headline': '婚恋 strict workflow 已识别支持层,但 timing conflict 仍要求降置信度。',
|
||||
'strengths': ['D9、UL 与部分 synastry taxonomy 已在场。'],
|
||||
'risks': ['dual dasha 与 external timing 发生冲突,不能把窗口直接抬成 legal marriage。'],
|
||||
'boundaries': ['存在 timing conflict 时,最终婚恋 narrative 必须明确降置信度。'],
|
||||
'markdown': '### 婚恋严格裁决\n- 当前 dual dasha 与 external timing 存在冲突,必须降置信度,不能把 supportive kuta 直接提升为 legal marriage。\n',
|
||||
},
|
||||
})
|
||||
|
||||
assert result['success'] is True
|
||||
html = Path(result['html_path']).read_text(encoding='utf-8')
|
||||
assert 'timing conflict' in html
|
||||
assert 'dual dasha' in html
|
||||
assert '降置信度' in html
|
||||
|
||||
|
||||
def test_report_artifact_relationship_strict_narrative_surfaces_public_formalization_candidate_boundary() -> None:
|
||||
handler = _handler()
|
||||
result = handler._compute_report_artifact({
|
||||
'format': 'html',
|
||||
'name': 'relationship-strict-public-formalization-report',
|
||||
'html': '<!doctype html><html><body><h1>Jyotish</h1></body></html>',
|
||||
'relationship_narrative': {
|
||||
'headline': '当前关系更接近 public_formalization candidate,而不是 legal marriage。',
|
||||
'strengths': ['公开化/可见度支持正在升温,但仍属于 context-only 线索。'],
|
||||
'risks': ['dual dasha 与 marriage convergence 还不足以把事件抬升为法律婚姻。'],
|
||||
'boundaries': ['public_formalization_candidate 只表示公开化候选,不等于法律婚姻,不能越权替代 legal_marriage。'],
|
||||
'markdown': '### 婚恋严格裁决\n- public_formalization_candidate 已进入 secondary-context,但仍不能替代 legal_marriage。\n',
|
||||
},
|
||||
})
|
||||
|
||||
assert result['success'] is True
|
||||
html = Path(result['html_path']).read_text(encoding='utf-8')
|
||||
assert 'public_formalization_candidate' in html
|
||||
assert '不等于法律婚姻' in html
|
||||
assert 'legal_marriage' in html
|
||||
|
||||
|
||||
def test_report_artifact_relationship_strict_narrative_warns_public_formalization_candidate_not_to_be_misread_as_near_marriage() -> None:
|
||||
handler = _handler()
|
||||
result = handler._compute_report_artifact({
|
||||
'format': 'html',
|
||||
'name': 'relationship-strict-public-formalization-conflict-report',
|
||||
'html': '<!doctype html><html><body><h1>Jyotish</h1></body></html>',
|
||||
'relationship_narrative': {
|
||||
'headline': '当前更接近 public_formalization candidate,但 timing conflict 仍然存在。',
|
||||
'strengths': ['公开化候选正在形成,但仍只是 context-only 层。'],
|
||||
'risks': ['当前 dual dasha / external timing 仍有冲突,不能误读成接近结婚。'],
|
||||
'boundaries': ['public_formalization_candidate 不等于法律婚姻,不能越权替代 legal_marriage。'],
|
||||
'markdown': '### 婚恋严格裁决\n- public_formalization_candidate 已进入 secondary-context,但当前 dual dasha 与 external timing 仍有冲突,不能误读成接近结婚,也不能替代 legal_marriage。\n',
|
||||
},
|
||||
})
|
||||
|
||||
assert result['success'] is True
|
||||
html = Path(result['html_path']).read_text(encoding='utf-8')
|
||||
assert 'public_formalization_candidate' in html
|
||||
assert '不能误读成接近结婚' in html
|
||||
assert 'legal_marriage' in html
|
||||
|
||||
|
||||
def test_report_artifact_relationship_strict_narrative_surfaces_weak_core_promise_guardrail_for_public_formalization_candidate() -> None:
|
||||
handler = _handler()
|
||||
result = handler._compute_report_artifact({
|
||||
'format': 'html',
|
||||
'name': 'relationship-strict-weak-core-promise-report',
|
||||
'html': '<!doctype html><html><body><h1>Jyotish</h1></body></html>',
|
||||
'relationship_narrative': {
|
||||
'headline': '当前更接近 public_formalization candidate,但 core marriage promise 仍偏弱。',
|
||||
'strengths': [
|
||||
'合盘支持已进入婚恋主链,但它只说明关系兼容度有帮助。',
|
||||
'公开化/关系可见度候选正在增强,但仍未达到法律婚姻落地。',
|
||||
],
|
||||
'risks': ['当前 core marriage promise 偏弱,不能误读成接近结婚。'],
|
||||
'boundaries': [
|
||||
'protective kuta support 只能辅助,不得越权抬升 legal_marriage。',
|
||||
'public_formalization_candidate 不等于法律婚姻。',
|
||||
],
|
||||
'markdown': '### 婚恋严格裁决\n- public_formalization_candidate 与 synastry_support 可以同时存在,但在 weak core marriage promise 下,仍不能写成婚姻逼近,也不能替代 legal_marriage。\n',
|
||||
},
|
||||
})
|
||||
|
||||
assert result['success'] is True
|
||||
html = Path(result['html_path']).read_text(encoding='utf-8')
|
||||
assert 'public_formalization_candidate' in html
|
||||
assert '合盘支持已进入婚恋主链' in html
|
||||
assert '不能误读成接近结婚' in html
|
||||
assert 'legal_marriage' in html
|
||||
assert 'relationship-caution' in html
|
||||
|
||||
|
||||
def test_report_artifact_pdf_fallback_exposes_user_visible_delivery(monkeypatch) -> None:
|
||||
class BrokenReportBuilder:
|
||||
@staticmethod
|
||||
@@ -832,9 +1010,37 @@ def test_thematic_report_derives_evidence_from_birth_payload() -> None:
|
||||
}
|
||||
assert 'chart' in marriage_sources
|
||||
assert 'full_reading.modules.marriage_counting' in marriage_sources
|
||||
assert 'full_reading.modules.relationship_strict_evidence.user_narrative' in marriage_sources
|
||||
assert any(item['details'].get('derived') for item in result['themes']['career']['evidence'])
|
||||
|
||||
|
||||
def test_thematic_report_derives_relationship_strict_narrative_evidence() -> None:
|
||||
handler = _handler()
|
||||
result = handler._compute_thematic_report({
|
||||
'theme': ['marriage'],
|
||||
'year': 1990,
|
||||
'month': 1,
|
||||
'day': 1,
|
||||
'hour': 12,
|
||||
'minute': 0,
|
||||
'lat': 39.9,
|
||||
'lon': 116.4,
|
||||
'tz': 8,
|
||||
})
|
||||
|
||||
assert result['success'] is True
|
||||
marriage_evidence = result['themes']['marriage']['evidence']
|
||||
strict_rows = [
|
||||
item for item in marriage_evidence
|
||||
if item['details'].get('source') == 'full_reading.modules.relationship_strict_evidence.user_narrative'
|
||||
]
|
||||
assert strict_rows
|
||||
strict_note = strict_rows[0]['conclusion']
|
||||
assert 'dual dasha' in strict_note
|
||||
assert 'D9' in strict_note
|
||||
assert 'legal_marriage' in strict_note or '婚恋' in strict_note
|
||||
|
||||
|
||||
def test_fragment_audit_blocks_registry_surface_drift() -> None:
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'scripts'))
|
||||
from audit_fragments import audit
|
||||
|
||||
@@ -987,6 +987,30 @@ def test_trust_center_surfaces_vedastro_adapter_status_without_endpoint_secret()
|
||||
assert "secret/path" not in main
|
||||
|
||||
|
||||
def test_trust_center_exposes_user_runnable_vedastro_range_scan() -> None:
|
||||
main = (ROOT / "jyotish-app" / "main.js").read_text(encoding="utf-8")
|
||||
api_bridge = (ROOT / "jyotish-app" / "api-bridge.js").read_text(encoding="utf-8")
|
||||
public_bridge = (ROOT / "jyotish-app" / "public" / "api-bridge.js").read_text(encoding="utf-8")
|
||||
|
||||
for bridge in (api_bridge, public_bridge):
|
||||
assert "runVedAstroRangeScan" in bridge
|
||||
assert "/api/vedastro/range_scan" in bridge
|
||||
for token in [
|
||||
"renderVedAstroUserScanPanel",
|
||||
"runVedAstroRangeScanFromPanel",
|
||||
"vedastro-run-range-scan",
|
||||
"vedastro-scan-domain",
|
||||
"vedastro-scan-start",
|
||||
"vedastro-scan-end",
|
||||
"VedAstro Range Scan",
|
||||
"modules.vedastro_range_scan_result",
|
||||
"service_endpoint_not_configured",
|
||||
"network_execution_disabled",
|
||||
"外部证据只进 secondary context",
|
||||
]:
|
||||
assert token in main
|
||||
|
||||
|
||||
def test_github_release_quality_gate_runs_browser_release_profile() -> None:
|
||||
workflow = (ROOT / ".github" / "workflows" / "release-quality-gate.yml").read_text(encoding="utf-8")
|
||||
for token in [
|
||||
@@ -1686,6 +1710,8 @@ def test_provenance_panchanga_workspace_panel_is_productized() -> None:
|
||||
"ulDkTiming",
|
||||
"UL/DK 与关系时机",
|
||||
"buildRelationshipReportTemplate",
|
||||
"public_formalization_candidate",
|
||||
"不能误读成接近结婚",
|
||||
"relationshipKutaMeaning",
|
||||
"renderRelationshipReport",
|
||||
"renderRelationshipReportList",
|
||||
@@ -1902,9 +1928,30 @@ def test_provenance_panchanga_workspace_panel_is_productized() -> None:
|
||||
assert "_relationshipReportBullets" in export_js
|
||||
assert "_relationshipReportList" in export_js
|
||||
assert "_relationshipBoundary" in export_js
|
||||
assert "_relationshipStrictNarrativeSection" in export_js
|
||||
assert "relationship_report" in export_js
|
||||
assert "relationship_narrative" in export_js
|
||||
assert "relationship_narrative" in main
|
||||
assert "strictNarrative" in main
|
||||
assert "relationship-deliverable" in export_js
|
||||
assert "relationship-evidence-grid" in export_js
|
||||
assert "relationship-strict-narrative" in export_js
|
||||
assert "relationship-caution" in export_js
|
||||
assert "婚恋严格裁决" in export_js
|
||||
assert "dual dasha" in export_js
|
||||
|
||||
|
||||
def test_synastry_relationship_report_template_keeps_public_formalization_candidate_as_context_not_near_marriage() -> None:
|
||||
main = read("main.js")
|
||||
export_js = read("export.js")
|
||||
html = read("index.html")
|
||||
manifest = read("public/manifest.webmanifest")
|
||||
sw = read("public/sw.js")
|
||||
glossary = read("glossary.js")
|
||||
|
||||
assert "public_formalization_candidate" in main
|
||||
assert "不能误读成接近结婚" in main
|
||||
assert "不得越权抬升 legal_marriage" in main
|
||||
assert "comparison-print-table" in export_js
|
||||
assert "composite-print-grid" in export_js
|
||||
assert "uldk-print-grid" in export_js
|
||||
@@ -1985,6 +2032,28 @@ def test_provenance_panchanga_workspace_panel_is_productized() -> None:
|
||||
assert "parseFloat($('birth-tz').value)" not in main
|
||||
assert "window.confirm(`删除" in main
|
||||
assert "window.confirm('清空本地星盘" in main
|
||||
assert "高兼容,仍需完整复核" in main
|
||||
assert "若当前更偏向 public_formalization_candidate,请把它理解为关系公开化候选,而不是婚姻逼近。" in main
|
||||
assert "公开化候选浮现,但婚姻承诺与时机仍需保守复核。" in main
|
||||
assert "公开化候选,不等于婚姻逼近" in main
|
||||
assert "先不要把高 Ashtakoot 分数翻译成婚姻逼近,应先复核 promise、dual dasha 与 external timing。" in main
|
||||
assert "status = hasPublicFormalizationCandidate && hasConflictWarning ? 'needs_context'" in main
|
||||
|
||||
|
||||
def test_synastry_relationship_report_template_keeps_high_ashtakoot_public_formalization_and_weak_promise_case_fully_conservative() -> None:
|
||||
main = read("main.js")
|
||||
|
||||
for token in [
|
||||
"高兼容,仍需完整复核",
|
||||
"public_formalization_candidate 说明当前更偏向公开化/关系可见度候选,而不是法律婚姻本身。",
|
||||
"当前即便存在合盘支持与公开化候选,也不能误读成接近结婚;若 weak core promise、dual dasha 或 external timing 未收敛,仍应保持保守。",
|
||||
"若当前更偏向 public_formalization_candidate,请把它理解为关系公开化候选,而不是婚姻逼近。",
|
||||
"先不要把高 Ashtakoot 分数翻译成婚姻逼近,应先复核 promise、dual dasha 与 external timing。",
|
||||
"public_formalization_candidate 只表示公开化候选,不得越权抬升 legal_marriage,也不能误读成接近结婚。",
|
||||
"公开化候选浮现,但婚姻承诺与时机仍需保守复核。",
|
||||
"公开化候选,不等于婚姻逼近",
|
||||
]:
|
||||
assert token in main
|
||||
|
||||
|
||||
def test_mobile_layout_keeps_dense_sections_single_column() -> None:
|
||||
|
||||
@@ -20,6 +20,8 @@ def test_life_event_graph_folds_strict_evidence_and_vedastro_top_event() -> None
|
||||
"ul_support",
|
||||
"external_activation_support",
|
||||
"synastry_support",
|
||||
"synastry_compatibility_support",
|
||||
"synastry_protective_kuta_support",
|
||||
],
|
||||
"primary_drivers": [
|
||||
"marriage_convergence",
|
||||
@@ -99,6 +101,16 @@ def test_life_event_graph_folds_strict_evidence_and_vedastro_top_event() -> None
|
||||
"tags": ["marriage", "transit"],
|
||||
"source": "vedastro_service_adapter_candidate",
|
||||
}
|
||||
assert {
|
||||
"kind": "context",
|
||||
"label": "synastry_compatibility_support",
|
||||
"source": "event_judgement.secondary_context",
|
||||
} in graph["event_nodes"]
|
||||
assert {
|
||||
"kind": "context",
|
||||
"label": "synastry_protective_kuta_support",
|
||||
"source": "event_judgement.secondary_context",
|
||||
} in graph["event_nodes"]
|
||||
|
||||
|
||||
def test_life_event_graph_is_returned_from_strict_relationship_evidence() -> None:
|
||||
|
||||
@@ -193,7 +193,7 @@ def test_vedastro_range_scan_unconfigured_still_returns_official_search_events_p
|
||||
assert report["request_preview"]["official_request_profile"]["method"] == "POST"
|
||||
assert report["request_preview"]["official_request_profile"]["headers"] == {"Content-Type": "application/json"}
|
||||
assert report["request_preview"]["official_request_profile"]["body"]["Ayanamsa"] == "lahiri"
|
||||
assert report["request_preview"]["official_request_profile"]["body"]["EventTagList"] == ["LendingMoney", "BorrowingMoney", "General"]
|
||||
assert report["request_preview"]["official_request_profile"]["body"]["EventTagList"] == ["LendingMoney", "BorrowingMoney", "BuyingSelling", "General"]
|
||||
assert "AtTime" not in report["request_preview"]["official_request_profile"]["body"]
|
||||
assert report["request_preview"]["official_request_profile"]["body"]["StartTime"]["StdTime"] == "12:00 01/01/2026 +08:00"
|
||||
assert report["request_preview"]["official_request_profile"]["body"]["EndTime"]["StdTime"] == "12:00 01/01/2031 +08:00"
|
||||
@@ -230,7 +230,7 @@ def test_vedastro_service_adapter_posts_official_search_events_contract() -> Non
|
||||
length = int(self.headers.get("Content-Length", "0"))
|
||||
payload = json.loads(self.rfile.read(length).decode("utf-8"))
|
||||
assert payload["Ayanamsa"] == "lahiri"
|
||||
assert payload["EventTagList"] == ["Marriage", "General"]
|
||||
assert payload["EventTagList"] == ["Marriage", "Personal", "General"]
|
||||
assert payload["BirthTime"]["StdTime"] == "12:00 01/01/1990 +08:00"
|
||||
assert payload["AtTime"]["StdTime"] == "12:00 01/01/2026 +08:00"
|
||||
assert "StartTime" not in payload
|
||||
@@ -369,7 +369,7 @@ def test_vedastro_service_adapter_can_normalize_mock_range_scan_response() -> No
|
||||
length = int(self.headers.get("Content-Length", "0"))
|
||||
payload = json.loads(self.rfile.read(length).decode("utf-8"))
|
||||
assert payload["Ayanamsa"] == "lahiri"
|
||||
assert payload["EventTagList"] == ["Marriage", "General"]
|
||||
assert payload["EventTagList"] == ["Marriage", "Personal", "General"]
|
||||
assert payload["BirthTime"]["StdTime"] == "12:00 01/01/1990 +08:00"
|
||||
assert payload["StartTime"]["StdTime"] == "12:00 01/01/2026 +08:00"
|
||||
assert payload["EndTime"]["StdTime"] == "12:00 01/01/2031 +08:00"
|
||||
@@ -531,7 +531,7 @@ def test_vedastro_range_scan_records_hashes_and_artifact_path() -> None:
|
||||
assert metadata["vedastro_event_method"] == "SearchEvents"
|
||||
assert metadata["official_endpoint_path"] == "/Calculate/SearchEvents"
|
||||
assert metadata["official_request_profile"]["method"] == "POST"
|
||||
assert metadata["official_request_profile"]["body"]["EventTagList"] == ["Marriage", "General"]
|
||||
assert metadata["official_request_profile"]["body"]["EventTagList"] == ["Marriage", "Personal", "General"]
|
||||
assert metadata["official_request_profile_hash"]
|
||||
assert metadata["allowlist_domain"] == "marriage"
|
||||
assert metadata["allowlist_event_count"] == 1
|
||||
@@ -691,6 +691,84 @@ def test_vedastro_service_adapter_applies_domain_allowlist_to_range_scan_noise()
|
||||
assert report["evidence_ledger"][0]["event_id"] == "GocharJupiterIn7th"
|
||||
|
||||
|
||||
def test_vedastro_service_adapter_preserves_match_metadata_for_official_tag_and_alias_hits() -> None:
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def do_POST(self) -> None: # noqa: N802
|
||||
response = {
|
||||
"Status": "Pass",
|
||||
"Payload": [
|
||||
{
|
||||
"Name": "GoodForMarriage",
|
||||
"Nature": "Good",
|
||||
"Description": "Marriage event support.",
|
||||
"StartTime": "2026-05-01",
|
||||
"EndTime": "2026-05-02",
|
||||
"EventTags": ["Marriage"],
|
||||
},
|
||||
{
|
||||
"Name": "PartnershipBlessingWindow",
|
||||
"Nature": "Good",
|
||||
"Description": "Spouse alignment and relationship blessing.",
|
||||
"StartTime": "2026-05-03",
|
||||
"EndTime": "2026-05-04",
|
||||
"EventTags": ["Personal"],
|
||||
},
|
||||
],
|
||||
}
|
||||
body = json.dumps(response).encode("utf-8")
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def log_message(self, format: str, *args) -> None: # noqa: A003
|
||||
return
|
||||
|
||||
server = HTTPServer(("127.0.0.1", 0), Handler)
|
||||
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
try:
|
||||
env = os.environ.copy()
|
||||
env["VEDASTRO_API_ENDPOINT"] = f"http://127.0.0.1:{server.server_port}/api"
|
||||
env["VEDASTRO_ENABLE_NETWORK"] = "1"
|
||||
completed = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
"scripts/vedastro_service_adapter.py",
|
||||
"--range-scan",
|
||||
"--domain",
|
||||
"marriage",
|
||||
"--case",
|
||||
"beijing_first_use_demo",
|
||||
"--start-date",
|
||||
"2026-01-01",
|
||||
"--end-date",
|
||||
"2026-12-31",
|
||||
],
|
||||
cwd=ROOT,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
timeout=120,
|
||||
check=False,
|
||||
env=env,
|
||||
)
|
||||
finally:
|
||||
server.shutdown()
|
||||
thread.join(timeout=5)
|
||||
|
||||
assert completed.returncode == 0, completed.stderr or completed.stdout
|
||||
report = json.loads(completed.stdout)
|
||||
assert report["event_count"] == 2
|
||||
exact = {item["event_id"]: item for item in report["evidence_ledger"]}
|
||||
assert exact["GoodForMarriage"]["matched_by"] == "official_tag"
|
||||
assert exact["PartnershipBlessingWindow"]["matched_by"] == "alias"
|
||||
assert exact["GoodForMarriage"]["confidence"] == "medium_high"
|
||||
assert exact["PartnershipBlessingWindow"]["confidence"] == "low"
|
||||
assert report["source_metadata"]["mapping_replay"]["match_counts"]["official_tag"] == 1
|
||||
assert report["source_metadata"]["mapping_replay"]["match_counts"]["alias"] == 1
|
||||
|
||||
|
||||
def test_vedastro_service_adapter_classifies_http_error() -> None:
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def do_POST(self) -> None: # noqa: N802
|
||||
|
||||
Reference in New Issue
Block a user