From 3161de68c03c9dded5c73a17f9bd601e2d42eabe Mon Sep 17 00:00:00 2001 From: 732642856 <732642856@qq.com> Date: Mon, 29 Jun 2026 07:51:00 +0800 Subject: [PATCH] Productize VedAstro user range scan entry --- .../2026-06-29-vedastro-user-range-scan.md | 63 ++++++ jyotish-app/api-bridge.js | 5 + jyotish-app/main.js | 145 ++++++++++++ jyotish-app/public/api-bridge.js | 5 + .../pyjhora_oracle_artifact_manifest.json | 2 +- scripts/jyotish_api_server.py | 70 ++++++ scripts/vedastro_service_adapter.py | 136 +++++++++++- task_plan.md | 3 + tests/test_api_server_security.py | 206 ++++++++++++++++++ tests/test_frontend_productization.py | 69 ++++++ tests/test_life_event_graph_v1.py | 12 + .../test_vedastro_service_adapter_executor.py | 86 +++++++- 12 files changed, 792 insertions(+), 10 deletions(-) create mode 100644 docs/superpowers/plans/2026-06-29-vedastro-user-range-scan.md diff --git a/docs/superpowers/plans/2026-06-29-vedastro-user-range-scan.md b/docs/superpowers/plans/2026-06-29-vedastro-user-range-scan.md new file mode 100644 index 00000000..4ae1e44c --- /dev/null +++ b/docs/superpowers/plans/2026-06-29-vedastro-user-range-scan.md @@ -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. diff --git a/jyotish-app/api-bridge.js b/jyotish-app/api-bridge.js index 2147ba36..b5fd5bbf 100644 --- a/jyotish-app/api-bridge.js +++ b/jyotish-app/api-bridge.js @@ -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, diff --git a/jyotish-app/main.js b/jyotish-app/main.js index 7b29fbcf..602f78cb 100644 --- a/jyotish-app/main.js +++ b/jyotish-app/main.js @@ -1806,6 +1806,7 @@ function renderTrustCenterPanel() { ${renderVedAstroStatus(vedastro)} ${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 ` +
+
+ VedAstro Range Scan + 对当前星盘运行外部高频事件雷达;外部证据只进 secondary context。 +
+
+ + + +
+
+ +
+ ${renderVedAstroRangeScanResult(state)} +
+ `; +} + +function renderVedAstroRangeScanResult(state = getVedAstroScanState()) { + if (state.status === 'running') { + return '
正在请求 /api/vedastro/range_scan...
'; + } + if (state.status === 'idle') { + return '
请先生成星盘,再选择年份或日期范围运行扫描。没有配置 VEDASTRO_API_ENDPOINT 时会返回 blocked 边界。
'; + } + if (state.status === 'error') { + return `
VedAstro Range Scan 未完成:${escapeHtml(state.error || '本地 API 不可用')}
`; + } + 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 ` +
+ VedAstro Range Scan:${escapeHtml(status)} · ${escapeHtml(String(eventCount))} events · ${escapeHtml(state.start_date || '-')}/${escapeHtml(state.end_date || '-')} +
+
+ ${escapeHtml(result.reason || state.result?.boundary || '外部雷达结果已挂到 chartData.modules.vedastro_range_scan_result;本地 Jyotish gates 仍为主判断。')} + ${metadata.artifact_path ? ` artifact: ${escapeHtml(metadata.artifact_path)}` : ''} +
+ ${events.length ? ` +
+ ${events.map(event => ` +
+ ${escapeHtml(event.signal_label || event.event_id || 'VedAstro event')} + ${escapeHtml(event.start || '-')} + ${escapeHtml(event.signal_family || event.domain || 'range_scan')} +
+ `).join('')} +
+ ` : ''} + `; +} + 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(); } diff --git a/jyotish-app/public/api-bridge.js b/jyotish-app/public/api-bridge.js index 2147ba36..b5fd5bbf 100644 --- a/jyotish-app/public/api-bridge.js +++ b/jyotish-app/public/api-bridge.js @@ -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, diff --git a/references/oracle/artifacts/pyjhora_oracle_artifact_manifest.json b/references/oracle/artifacts/pyjhora_oracle_artifact_manifest.json index 9ca7f84a..5bf73d39 100644 --- a/references/oracle/artifacts/pyjhora_oracle_artifact_manifest.json +++ b/references/oracle/artifacts/pyjhora_oracle_artifact_manifest.json @@ -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": [ diff --git a/scripts/jyotish_api_server.py b/scripts/jyotish_api_server.py index c9893de2..461003d6 100644 --- a/scripts/jyotish_api_server.py +++ b/scripts/jyotish_api_server.py @@ -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): diff --git a/scripts/vedastro_service_adapter.py b/scripts/vedastro_service_adapter.py index 66b6d17a..dc5be9f9 100644 --- a/scripts/vedastro_service_adapter.py +++ b/scripts/vedastro_service_adapter.py @@ -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 = { diff --git a/task_plan.md b/task_plan.md index 988648b8..abaca0f1 100644 --- a/task_plan.md +++ b/task_plan.md @@ -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 同步审计和浏览器用户流验证,避免与核心计算修改冲突。 diff --git a/tests/test_api_server_security.py b/tests/test_api_server_security.py index 4cff2382..fe4bb6f9 100644 --- a/tests/test_api_server_security.py +++ b/tests/test_api_server_security.py @@ -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': '

Jyotish

', + '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': '

Jyotish

', + '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': '

Jyotish

', + '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': '

Jyotish

', + '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': '

Jyotish

', + '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 diff --git a/tests/test_frontend_productization.py b/tests/test_frontend_productization.py index 567639d1..39f1ba36 100644 --- a/tests/test_frontend_productization.py +++ b/tests/test_frontend_productization.py @@ -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: diff --git a/tests/test_life_event_graph_v1.py b/tests/test_life_event_graph_v1.py index 90a3ae50..74d297dc 100644 --- a/tests/test_life_event_graph_v1.py +++ b/tests/test_life_event_graph_v1.py @@ -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: diff --git a/tests/test_vedastro_service_adapter_executor.py b/tests/test_vedastro_service_adapter_executor.py index d6d551f9..533a8283 100644 --- a/tests/test_vedastro_service_adapter_executor.py +++ b/tests/test_vedastro_service_adapter_executor.py @@ -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