diff --git a/docs/research/pre_work_error_ledger.md b/docs/research/pre_work_error_ledger.md index 77f3f432..ad2d5dde 100644 --- a/docs/research/pre_work_error_ledger.md +++ b/docs/research/pre_work_error_ledger.md @@ -62,6 +62,8 @@ For large architecture or release work, also read: | ERR-029 | Basic git and premium cloud-drive skill packages can blur contents, privacy exclusions, and external-engine promises. | mitigated 2026-07-09 | `scripts/skill_release_manifest.py` must define edition contents, excluded private material, acceptance commands, and external-engine runtime boundaries before packaging. | | ERR-030 | Release packaging can misread non-ASCII tracked filenames when parsing quoted `git ls-files` output. | mitigated 2026-07-09 | Package builders must use `git ls-files -z` and decode NUL-separated paths before writing zip archives. | | ERR-031 | Premium skill zip can ship without user install prompts or replay schemas, leaving users and future oracle imports without a contract. | mitigated 2026-07-09 | `skill_release_package.py` must inject `INSTALL.md` and `USER_PROMPTS.md`; replay contracts must live in `references/real_case_calibration/` and `references/oracle/`. | +| ERR-032 | Full smoke files can time out while focused slices pass; `test_full_reading_reports_ayanamsa_metadata_and_ai_prompt_pack` currently exposes `external_oracle_gap_summary=null`. | observed 2026-07-10 | Do not claim full `tests/test_cli_smoke.py` or full `tests/test_vedastro_external_technique_evidence.py` passed unless run to completion; use focused slices for related changes and track the prompt-pack gap separately. | +| ERR-033 | Premium skill zip validation can accidentally depend on a parent Git repository, so a cloud-drive user may fail in a clean unzip directory. | mitigated 2026-07-10 | Release acceptance must include `tests/test_skill_release_clean_trial.py`; scripts such as `public_release_privacy_scan.py` must support non-Git unpacked zip directories. | ## Fragment Sweep Command Set diff --git a/mcp_server.py b/mcp_server.py index 87fa074d..c26d56db 100644 --- a/mcp_server.py +++ b/mcp_server.py @@ -712,6 +712,8 @@ def _execute_mcp_consultation_workflow( entry_mode: str = "direct_chart", theme: list[str] | None = None, events: list[dict[str, Any]] | None = None, + western_evidence_packet: dict[str, Any] | None = None, + western_oracle_payload: dict[str, Any] | None = None, ) -> Dict[str, Any]: from jyotish_api_server import JyotishAPIHandler, execute_consultation_workflow @@ -733,6 +735,8 @@ def _execute_mcp_consultation_workflow( "entry_mode": entry_mode, "theme": theme or [], "events": events or [], + **({"western_evidence_packet": western_evidence_packet} if isinstance(western_evidence_packet, dict) else {}), + **({"western_oracle_payload": western_oracle_payload} if isinstance(western_oracle_payload, dict) else {}), }, surface="skill_mcp", ) @@ -4186,6 +4190,8 @@ def strict_workflow( age: int, transit_date: str, node_mode: str = "mean", + western_evidence_packet: Optional[Dict[str, Any]] = None, + western_oracle_payload: Optional[Dict[str, Any]] = None, ) -> Dict[str, Any]: """ Strict workflow router: routes question to the correct analysis path. @@ -4208,6 +4214,8 @@ def strict_workflow( age: Current age transit_date: Transit date for prediction (YYYY-MM-DD) node_mode: 'mean' or 'true' + western_evidence_packet: Optional pre-normalized Western evidence packet + western_oracle_payload: Optional external Western oracle JSON export Returns: JSON with routed analysis and confidence level @@ -4229,6 +4237,8 @@ def strict_workflow( node_mode=node_mode, entry_mode="direct_chart", theme=normalized_themes, + western_evidence_packet=western_evidence_packet, + western_oracle_payload=western_oracle_payload, ) chart = result.get("chart") if isinstance(result, dict) else {} route = _safe_get(result, "routing", "question_type") or route_packet["question_type"] or "general" @@ -4276,6 +4286,9 @@ def strict_workflow( result["interpretation_source_runtime_coverage"] = interpretation_coverage result["machine_evidence_packet"] = machine_evidence_packet result["real_case_calibration"] = real_case_calibration + western_packet = result.get("western_evidence_packet") + if not isinstance(western_packet, dict) or not western_packet: + western_packet = western_evidence_packet if isinstance(western_evidence_packet, dict) else None result["runtime_evidence_log"] = _UNIFIED_CONSULTATION_ORCHESTRATOR.runtime_evidence_log( surface="skill_mcp", entry_mode=result.get("entry_mode", "direct_chart"), @@ -4286,6 +4299,7 @@ def strict_workflow( interpretation_source_runtime_coverage=interpretation_coverage, machine_evidence_packet=machine_evidence_packet, real_case_calibration=real_case_calibration, + western_evidence_packet=western_packet, blind=False, ) except Exception: diff --git a/progress.md b/progress.md index 57d4ca84..7a533574 100644 --- a/progress.md +++ b/progress.md @@ -876,3 +876,72 @@ - 已启用 Hermes fallback 插件:`hermes plugins enable squeez-fallback` 成功,提示下一 session 生效。 - 已验证 `ccusage` 可通过 `npx` 临时运行,版本 `20.0.14`;本轮未全局安装。 - 仍需用户重启 Codex/相关 CLI,让已安装 hooks 在新 session 中生效。 + +## 2026-07-10T00:00:00+08:00 - 西占/印占协同仲裁层 MVP + +- 用户要求按规划执行“印度占星主链 + 西方占星交叉验证”的高级版能力。 +- 新增 `scripts/western_evidence_packet.py`:定义西占证据包最小合同,只接收已生成的本命/时序/信号证据,不内置 Kerykeion、pyswisseph、Flatlib、Immanuel 等外部项目源码。 +- 新增 `scripts/cross_system_arbitrator.py`:比较 Jyotish 与 Western evidence signals,输出 shared signals、conflicts、blocked items 和 Technique Audit rows。 +- `scripts/unified_consultation_orchestrator.py` 的 `runtime_evidence_log()` 新增可选 `western_evidence_packet`,并把 `Western Cross-Validation`、`Cross-System Arbitration` 纳入 Technique Audit Table 与 required rows。 +- 新增测试:`tests/test_western_evidence_packet.py`、`tests/test_cross_system_arbitrator.py`,并扩展 `tests/test_unified_consultation_orchestrator.py`。 +- 验证:`python3 -m pytest -q tests/test_western_evidence_packet.py tests/test_cross_system_arbitrator.py tests/test_unified_consultation_orchestrator.py tests/test_mcp_strict_workflow_career.py tests/test_vedastro_external_technique_evidence.py::test_vedastro_adapter_declares_external_technique_evidence_policy tests/test_vedastro_external_technique_evidence.py::test_vedastro_adapter_builds_external_technique_preview_without_network` 通过,39 passed;`python3 scripts/pre_work_check.py --remote-timeout 8 --command-timeout 45` 通过。 +- 边界:本轮不声称已完成西占排盘引擎内置或真实案例 replay complete;外部西占项目仍按 license boundary 作为 oracle/adapters,不直接复制进高级闭源包。 + +## 2026-07-10T00:00:00+08:00 - 西占外部 oracle JSON adapter + +- 新增 `scripts/western_oracle_adapter.py`:读取外部西占 JSON,输出标准 `western_evidence_packet`。 +- 支持两种输入:显式 `signals` 直通;或从 `aspects` 派生少量可审计 signals。 +- 当前派生规则只覆盖三类高价值 career signals:Uranus 合 MC、Jupiter 拱/六合 Mercury/Venus/MC、Saturn 合 Sun/Mercury/Venus。 +- 新增 `references/oracle/western_oracle_adapter_contract.md`,说明输入格式、CLI、派生规则和许可证边界。 +- 新增 `tests/test_western_oracle_adapter.py`。 +- 验证:`python3 -m pytest -q tests/test_western_oracle_adapter.py tests/test_western_evidence_packet.py tests/test_cross_system_arbitrator.py tests/test_unified_consultation_orchestrator.py` 通过,22 passed;MCP/外部证据 focused 20 passed。 +- 边界:adapter 不执行西占排盘,不内置 Kerykeion/Flatlib/Immanuel/pyswisseph;这些仍作为外部 oracle 或用户导出 JSON 来源。 + +## 2026-07-10T00:00:00+08:00 - 西占 oracle 入口闭环 + +- API 共享执行器 `execute_consultation_workflow(...)` 现在接受 `western_evidence_packet` 或 `western_oracle_payload`,会把外部西占导出 JSON 标准化后传入 `runtime_evidence_log()`。 +- MCP `strict_workflow(...)` 现在也可透传同样的西占证据包/外部 oracle payload,并在二次 strict adjudication 后保留 `Western Cross-Validation` 与 `Cross-System Arbitration` audit。 +- `machine_evidence_packet()` 现在保留上游已生成的 `cross_system_signals`,只做证据透传,不自行生成解释性结论。 +- `references/oracle/western_oracle_adapter_contract.md` 已补 API/MCP 入口字段说明。 +- 新增入口测试:`test_consultation_workflow_accepts_western_oracle_payload`、`test_mcp_strict_workflow_preserves_western_evidence_packet`。 +- 验证:`python3 -m pytest -q tests/test_api_server_security.py::test_consultation_workflow_accepts_western_oracle_payload tests/test_mcp_strict_workflow_career.py::test_mcp_strict_workflow_preserves_western_evidence_packet tests/test_western_oracle_adapter.py tests/test_western_evidence_packet.py tests/test_cross_system_arbitrator.py tests/test_unified_consultation_orchestrator.py` 通过,24 passed;扩展 MCP/API focused 回归通过,24 passed。 +- 边界:若 Jyotish 侧没有生成同主题 `cross_system_signals`,cross-system arbitration 仍会诚实显示 `partial`,不会把单边西占信号伪装成双系统互证。 + +## 2026-07-10T00:00:00+08:00 - 真实案例 outcome replay manifest 守门 + +- 新增 `references/real_case_calibration/replay_manifest.json`:先固定真实案例 outcome replay 导入合同;当前 `cases=[]`,状态明确 blocked,不伪装为 complete。 +- 新增 `scripts/real_case_replay_validator.py`:无第三方依赖,检查 case/source/event/replay 必填字段,输出 `case_count`、`replay_ready_count`、`blocked_reason` 和结构错误列表。 +- `real_case_calibration_catalog()` 现在暴露 `required_replay_schema` 与 `outcome_replay_manifest`,让 runtime evidence 能显示真实案例 replay 是否真的完成。 +- 新增测试:`tests/test_real_case_replay_validator.py`。 +- 验证:`python3 -m pytest -q tests/test_real_case_replay_validator.py tests/test_calibration_replay_schemas.py tests/test_unified_consultation_orchestrator.py tests/test_western_oracle_adapter.py tests/test_cross_system_arbitrator.py tests/test_api_server_security.py::test_consultation_workflow_accepts_western_oracle_payload tests/test_mcp_strict_workflow_career.py::test_mcp_strict_workflow_preserves_western_evidence_packet` 通过,27 passed。 +- 边界:本轮只完成 replay 合同与机器守门;尚未导入全球真实案例 raw/outcome 数据,因此 `Real Case Calibration` 仍应保持 partial/blocked 置信边界。 + +## 2026-07-10T00:00:00+08:00 - 三引擎 parity replay manifest 守门 + +- 新增 `references/oracle/three_engine_parity_replay_manifest.json`:固定 VedAstro / PyJHora-JHora / jyotishganit 同盘 replay 导入合同;当前无 comparison rows,明确 blocked。 +- 新增 `scripts/three_engine_parity_replay_validator.py`:无第三方依赖,校验 required engines、comparison rows、row status,输出 `tested`、match/mismatch/blocked counts 和 `blocked_reason`。 +- `scripts/diagnose_external_engine_adapters.py` 的 `same_chart_parity_contract` 现在包含 `replay_manifest`,避免把 readiness 误读成真实同盘 parity 已测试。 +- 新增测试:`tests/test_three_engine_parity_replay_validator.py`,并扩展 `tests/test_external_engine_adapter_diagnostics.py`。 +- 验证:`python3 -m pytest -q tests/test_unified_consultation_orchestrator.py tests/test_western_oracle_adapter.py tests/test_cross_system_arbitrator.py tests/test_api_server_security.py::test_consultation_workflow_accepts_western_oracle_payload tests/test_mcp_strict_workflow_career.py::test_mcp_strict_workflow_preserves_western_evidence_packet tests/test_external_engine_adapter_diagnostics.py tests/test_three_engine_parity_replay_validator.py tests/test_real_case_replay_validator.py` 通过,28 passed。 +- 边界:本轮只完成 parity replay 合同与诊断守门;未安装/调用外部专业软件,不声称 VedAstro/PyJHora/JHora/jyotishganit 已完成同盘比对。 + +## 2026-07-10T00:00:00+08:00 - 高级版 Skill 打包最终验收 + +- `scripts/skill_release_package.py` 现在会生成 `RELEASE_MANIFEST.json` 和 `PACKAGE_ACCEPTANCE.json`,并把 acceptance 状态写入 dry-run/zip plan。 +- `INSTALL.md` 生成内容已补基础 Git 地址 `https://github.com/732642856/yinduzhanxing`、普通用户验收命令和 VedAstro official raw 边界。 +- `USER_PROMPTS.md` 生成内容已补最高品质调用模板:统一主链、Technique Audit Table、三引擎 parity replay、真实案例 replay、`western_oracle_payload` 边界。 +- 高级版 zip 会强制包含三类合同:`references/real_case_calibration/replay_manifest.json`、`references/oracle/three_engine_parity_replay_manifest.json`、`references/oracle/western_oracle_adapter_contract.md`,即使这些合同是当前工作区新文件。 +- 修复 `scripts/user_invocation_acceptance_check.py`:外部引擎诊断 `status=complete` 现在视为可用状态,不再误报 `external_adapter_status_unusable`。 +- 已生成本地验收产物:`scratch/local/releases/jyotish-premium-skill.zip`,大小约 20 MB,`PACKAGE_ACCEPTANCE.json.status=pass`,关键文件缺失列表为空。 +- 验证:`python3 -m pytest -q tests/test_skill_release_package.py tests/test_skill_release_manifest.py tests/test_user_invocation_acceptance_contract.py tests/test_external_engine_adapter_diagnostics.py tests/test_three_engine_parity_replay_validator.py tests/test_real_case_replay_validator.py` 通过,14 passed;`git diff --check` 通过;`python3 scripts/pre_work_check.py --remote-timeout 8 --command-timeout 45` 通过。 +- 边界:zip 产物只在本地 scratch,尚未提交或推送;外部 official raw/parity rows 未导入,包内 acceptance 只证明 release hygiene 和合同齐备。 + +## 2026-07-10T00:00:00+08:00 - 高级版 Skill 干净目录解压试用验收 + +- 新增 `tests/test_skill_release_clean_trial.py`:测试会生成高级版 zip、解压到 pytest 临时干净目录,再在解压目录中运行隐私扫描和普通用户调用验收。 +- 修复 `scripts/public_release_privacy_scan.py`:当目录没有 `.git` 或 `git ls-files` 不可用时,自动退回目录遍历;这覆盖云盘 zip 解压后的真实用户环境。 +- 新增 `test_public_release_privacy_scan_supports_unpacked_zip_without_git`,防止隐私扫描再次只适配源码仓。 +- 手工 clean trial 已跑过:`scratch/local/clean_skill_trial` 关键文件缺失列表为空,`public_release_privacy_scan.py` 0 findings,`diagnose_external_engine_adapters.py --json` 返回 `status=complete`,`user_invocation_acceptance_check.py` 返回 `status=pass`。 +- 已用修复后的脚本重新生成 `scratch/local/releases/jyotish-premium-skill.zip`;并在仓外 `/tmp/jyotish-clean-trial.MvZWWX` 解压验证,确认无 `.git` 环境下 `public_release_privacy_scan.py`、`diagnose_external_engine_adapters.py --json`、`user_invocation_acceptance_check.py` 均可运行,用户调用验收 `status=pass`。 +- 验证:`python3 -m pytest -q tests/test_public_release_privacy_scan.py tests/test_skill_release_clean_trial.py` 通过,4 passed;release 组合回归 `python3 -m pytest -q tests/test_skill_release_clean_trial.py tests/test_skill_release_package.py tests/test_skill_release_manifest.py tests/test_user_invocation_acceptance_contract.py tests/test_external_engine_adapter_diagnostics.py tests/test_three_engine_parity_replay_validator.py tests/test_real_case_replay_validator.py tests/test_public_release_privacy_scan.py` 通过,18 passed。 +- 边界:这是本地 macOS/Python 干净目录验收,不代表 VedAstro official raw、PyJHora/JHora 或真实案例 replay 已完成外部数据闭环。 diff --git a/references/oracle/three_engine_parity_replay_manifest.json b/references/oracle/three_engine_parity_replay_manifest.json new file mode 100644 index 00000000..ec4cdd97 --- /dev/null +++ b/references/oracle/three_engine_parity_replay_manifest.json @@ -0,0 +1,25 @@ +{ + "case_id": "contract_ready_no_same_chart_rows", + "birth_data_policy": "operator_local_only", + "status": "not_started", + "engines": { + "VedAstro": { + "status": "blocked", + "official_raw_response_path": "", + "artifact_hash": "" + }, + "PyJHora_JHora": { + "status": "blocked", + "raw_output_path": "", + "settings": {} + }, + "jyotishganit": { + "status": "blocked", + "raw_output_path": "", + "settings": {} + } + }, + "comparison_rows": [], + "blocked_reason": "no_same_chart_oracle_rows_imported", + "runtime_boundary": "This manifest defines the same-chart parity replay contract only. It does not prove VedAstro/PyJHora/JHora/jyotishganit parity until raw oracle rows are imported." +} diff --git a/references/oracle/western_oracle_adapter_contract.md b/references/oracle/western_oracle_adapter_contract.md new file mode 100644 index 00000000..9469c386 --- /dev/null +++ b/references/oracle/western_oracle_adapter_contract.md @@ -0,0 +1,76 @@ +# Western Oracle Adapter Contract + +This contract defines how external Western astrology outputs enter the high-rigor Jyotish workflow. + +## Purpose + +`scripts/western_oracle_adapter.py` converts an external Western astrology JSON export into the standard `western_evidence_packet`. + +It is an evidence adapter, not a bundled Western astrology engine. + +## Accepted Input + +```json +{ + "source_engine": "kerykeion_external_json", + "natal": { + "ascendant": "Virgo", + "mc": "Gemini" + }, + "timing_techniques": { + "solar_return": { + "annual_focus": "career" + } + }, + "aspects": [ + { + "date": "2026-07-07", + "planet": "Uranus", + "aspect": "conjunction", + "target": "MC" + } + ] +} +``` + +Explicit `signals` may be provided. When present, explicit signals are preserved and aspect-derived signals are not guessed. + +## Derived Signal Rules + +The adapter currently maps only a small auditable subset: + +| Aspect Pattern | Signal | +|---|---| +| Uranus conjunction MC/Midheaven | `career_relocation / career_triggered_relocation` | +| Jupiter trine/sextile Mercury/Venus/MC | `career / client_cooperation_opportunity` | +| Saturn conjunction Sun/Mercury/Venus | `career / career_responsibility_test` | + +More mappings require tests and source notes before being added. + +## Entry Points + +API and MCP callers may pass either field: + +- `western_oracle_payload`: raw external Western astrology JSON export; the project normalizes it with `western_oracle_adapter`. +- `western_evidence_packet`: pre-normalized packet; the project carries it directly into `runtime_evidence_log`. + +When Jyotish evidence also carries matching `cross_system_signals`, `Cross-System Arbitration` can become `used`. If only Western evidence is present, arbitration stays `partial` or `blocked`. + +## CLI + +```bash +python3 scripts/western_oracle_adapter.py \ + --input /path/to/western_oracle.json \ + --theme career \ + --question-type career +``` + +The command prints a `western_evidence_packet` JSON object. + +## License Boundary + +The premium skill may accept JSON exports from Kerykeion, Flatlib, Immanuel, Astro.com, GongShenXing, or desktop software. + +Do not copy third-party source code into the premium package unless the exact project and dependency licenses permit bundling. + +The adapter stores derived evidence and source labels only. diff --git a/references/real_case_calibration/replay_manifest.json b/references/real_case_calibration/replay_manifest.json new file mode 100644 index 00000000..42ad725a --- /dev/null +++ b/references/real_case_calibration/replay_manifest.json @@ -0,0 +1,8 @@ +{ + "schema_version": "1.0", + "status": "contract_ready_no_cases", + "case_schema": "references/real_case_calibration/catalog.schema.json", + "cases": [], + "blocked_reason": "no_structured_outcome_replay_cases_imported", + "runtime_boundary": "This manifest defines the replay import contract only. No real-case outcome replay is complete until structured cases are imported and validated." +} diff --git a/scripts/cross_system_arbitrator.py b/scripts/cross_system_arbitrator.py new file mode 100644 index 00000000..741da68e --- /dev/null +++ b/scripts/cross_system_arbitrator.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Cross-system Jyotish/Western evidence arbitration.""" + +from __future__ import annotations + +from typing import Any + + +def _packet_status(packet: dict[str, Any] | None) -> str: + if not isinstance(packet, dict) or not packet: + return "blocked" + return str(packet.get("status") or "partial") + + +def _signals(packet: dict[str, Any] | None) -> list[dict[str, Any]]: + if not isinstance(packet, dict): + return [] + raw = packet.get("signals") + if not isinstance(raw, list): + raw = packet.get("cross_system_signals") + if not isinstance(raw, list): + return [] + signals: list[dict[str, Any]] = [] + for item in raw: + if isinstance(item, dict) and item.get("theme") and item.get("claim"): + signals.append( + { + "theme": str(item.get("theme")), + "claim": str(item.get("claim")), + "timing": str(item.get("timing") or "unspecified"), + "source": str(item.get("source") or "unspecified"), + } + ) + return signals + + +def _audit_row(name: str, status: str, used: bool, effect: str) -> dict[str, Any]: + return { + "technique": name, + "status": status, + "used": used, + "effect_on_confidence": effect, + } + + +def build_cross_system_arbitration( + *, + route_packet: dict[str, Any], + jyotish_evidence: dict[str, Any] | None, + western_evidence: dict[str, Any] | None, +) -> dict[str, Any]: + """Compare materialized Jyotish and Western evidence packets.""" + + primary_theme = str(route_packet.get("primary_theme") or route_packet.get("question_type") or "general") + jyotish_status = _packet_status(jyotish_evidence) + western_status = _packet_status(western_evidence) + blocked_items: list[str] = [] + if jyotish_status == "blocked": + blocked_items.append("jyotish_evidence_packet_missing") + if western_status == "blocked": + blocked_items.append("western_evidence_packet_missing") + + jyotish_signals = _signals(jyotish_evidence) + western_signals = _signals(western_evidence) + shared: list[dict[str, Any]] = [] + conflicts: list[dict[str, Any]] = [] + + for jyotish_signal in jyotish_signals: + for western_signal in western_signals: + if jyotish_signal["theme"] != western_signal["theme"]: + continue + if jyotish_signal["claim"] == western_signal["claim"]: + shared.append( + { + "theme": jyotish_signal["theme"], + "claim": jyotish_signal["claim"], + "timing": ( + jyotish_signal["timing"] + if jyotish_signal["timing"] == western_signal["timing"] + else f"{jyotish_signal['timing']} | western:{western_signal['timing']}" + ), + "jyotish_source": jyotish_signal["source"], + "western_source": western_signal["source"], + "confidence_effect": "raises_confidence", + } + ) + else: + conflicts.append( + { + "theme": jyotish_signal["theme"], + "jyotish_claim": jyotish_signal["claim"], + "western_claim": western_signal["claim"], + "jyotish_timing": jyotish_signal["timing"], + "western_timing": western_signal["timing"], + "confidence_effect": "lowers_confidence", + } + ) + + if blocked_items: + status = "blocked" + elif conflicts and not shared: + status = "conflict" + elif shared: + status = "used" + else: + status = "partial" + + return { + "status": status, + "primary_theme": primary_theme, + "jyotish_cross_validation": {"status": jyotish_status, "signal_count": len(jyotish_signals)}, + "western_cross_validation": {"status": western_status, "signal_count": len(western_signals)}, + "shared_signals": shared, + "conflicts": conflicts, + "blocked_items": blocked_items, + "technique_audit_rows": [ + _audit_row( + "Western Cross-Validation", + western_status, + western_status in {"complete", "used"} or bool(western_signals), + "western_oracle_available_for_trigger_refinement" + if western_status != "blocked" + else "blocked_until_western_evidence_packet_is_materialized", + ), + _audit_row( + "Cross-System Arbitration", + status, + status == "used", + "shared_signals_raise_confidence" + if status == "used" + else "confidence_capped_until_systems_share_a_theme_signal", + ), + ], + "boundary": ( + "Arbitration compares materialized evidence only; it does not replace Jyotish adjudication or " + "claim Western calculations were run when the western packet is missing." + ), + } diff --git a/scripts/diagnose_external_engine_adapters.py b/scripts/diagnose_external_engine_adapters.py index 1b6284bc..e3d1551c 100644 --- a/scripts/diagnose_external_engine_adapters.py +++ b/scripts/diagnose_external_engine_adapters.py @@ -10,10 +10,12 @@ try: from diagnose_vedastro_mode import build_report as build_vedastro_report from diagnose_pyjhora_adapter import build_report as build_pyjhora_report from diagnose_jyotishganit_adapter import build_report as build_jyotishganit_report + from three_engine_parity_replay_validator import validate_manifest as validate_parity_replay_manifest except Exception: # pragma: no cover - import path varies in tests/CLI from scripts.diagnose_vedastro_mode import build_report as build_vedastro_report from scripts.diagnose_pyjhora_adapter import build_report as build_pyjhora_report from scripts.diagnose_jyotishganit_adapter import build_report as build_jyotishganit_report + from scripts.three_engine_parity_replay_validator import validate_manifest as validate_parity_replay_manifest REQUIRED_PARITY_OUTPUTS = ["D1", "D9", "D10", "D2", "D4", "Vimshottari", "Shadbala", "Ashtakavarga"] @@ -29,6 +31,7 @@ def _same_chart_parity_contract(engines: dict) -> dict: "blocked": not available, "blocking_reason": "" if available else engine["status"], } + replay_manifest = validate_parity_replay_manifest("references/oracle/three_engine_parity_replay_manifest.json") return { "status": "ready" if all(state["available"] for state in engine_states.values()) else "blocked", "required_outputs": REQUIRED_PARITY_OUTPUTS, @@ -56,6 +59,7 @@ def _same_chart_parity_contract(engines: dict) -> dict: "jyotishganit": ["raw_output_path", "panchanga", "tithi", "nakshatra", "yoga", "karana"], }, "engine_states": engine_states, + "replay_manifest": replay_manifest, "boundary": "This is a parity contract, not proof that the same-chart comparison has run.", } diff --git a/scripts/jyotish_api_server.py b/scripts/jyotish_api_server.py index 1465c6c9..e0767f59 100644 --- a/scripts/jyotish_api_server.py +++ b/scripts/jyotish_api_server.py @@ -30,6 +30,10 @@ try: from scripts.unified_consultation_orchestrator import UnifiedConsultationOrchestrator except ModuleNotFoundError: # pragma: no cover - script execution path from unified_consultation_orchestrator import UnifiedConsultationOrchestrator +try: + from scripts.western_oracle_adapter import build_packet_from_oracle_payload +except ModuleNotFoundError: # pragma: no cover - script execution path + from western_oracle_adapter import build_packet_from_oracle_payload SCRIPTS_DIR = os.path.dirname(os.path.abspath(__file__)) REPO_ROOT = os.path.abspath(os.path.join(SCRIPTS_DIR, '..')) @@ -41,6 +45,27 @@ _HIGH_RIGOR_JOB_SCOPE = 'high_rigor_workflow' _UNIFIED_CONSULTATION_ORCHESTRATOR = UnifiedConsultationOrchestrator() +def _western_evidence_packet_from_body(body: dict, route_packet: dict) -> dict | None: + explicit_packet = body.get('western_evidence_packet') + if isinstance(explicit_packet, dict): + return explicit_packet + oracle_payload = body.get('western_oracle_payload') or body.get('western_astrology_oracle') + if not isinstance(oracle_payload, dict): + return None + try: + return build_packet_from_oracle_payload(oracle_payload, route_packet=route_packet) + except Exception as exc: # pragma: no cover - defensive contract boundary + return { + 'system': 'western_astrology', + 'status': 'blocked', + 'route': dict(route_packet), + 'signals': [], + 'missing_sections': ['western_oracle_payload'], + 'adapter_error': exc.__class__.__name__, + 'boundary': 'Western oracle payload was supplied but could not be normalized.', + } + + def execute_consultation_workflow( handler, *, @@ -55,6 +80,7 @@ def execute_consultation_workflow( entry_mode = body.get('entry_mode', 'direct_chart') high_rigor = bool(body.get('return_high_rigor_shape')) route_packet = _UNIFIED_CONSULTATION_ORCHESTRATOR.resolve_route(question, themes) + western_evidence_packet = _western_evidence_packet_from_body(body, route_packet) unified_contract = _UNIFIED_CONSULTATION_ORCHESTRATOR.shared_contract( entry_mode=entry_mode, question=question, @@ -97,8 +123,11 @@ def execute_consultation_workflow( route_packet=route_packet, executed_steps=[], skipped_steps=known_steps, + western_evidence_packet=western_evidence_packet, blind=bool(body.get('blind') or body.get('blind_technical_mode')), ) + if western_evidence_packet: + result['western_evidence_packet'] = western_evidence_packet if body.get('return_high_rigor_shape'): result['endpoint'] = 'high_rigor_workflow' return result @@ -207,6 +236,7 @@ def execute_consultation_workflow( interpretation_source_runtime_coverage=interpretation_source_runtime_coverage, machine_evidence_packet=machine_evidence_packet, real_case_calibration=real_case_calibration, + western_evidence_packet=western_evidence_packet, blind=bool(body.get('blind') or body.get('blind_technical_mode')), ) @@ -253,6 +283,7 @@ def execute_consultation_workflow( 'runtime_truth': runtime_truth, 'interpretation_source_runtime_coverage': interpretation_source_runtime_coverage, 'machine_evidence_packet': machine_evidence_packet, + 'western_evidence_packet': western_evidence_packet or {}, 'real_case_calibration': real_case_calibration, 'runtime_evidence_log': runtime_evidence_log, 'next_questions': handler._high_rigor_next_questions(rectification, historical_backtest), diff --git a/scripts/public_release_privacy_scan.py b/scripts/public_release_privacy_scan.py index e0d907fe..b71580c7 100644 --- a/scripts/public_release_privacy_scan.py +++ b/scripts/public_release_privacy_scan.py @@ -5,6 +5,7 @@ from __future__ import annotations import argparse import json +import os import re import subprocess from pathlib import Path @@ -40,28 +41,38 @@ TEXT_SUFFIXES = { ".yml", } -DENY_PATTERNS: list[tuple[str, re.Pattern[str]]] = [ - ("private_exact_iso_birth_date", re.compile(r"REDACTED_DATE")), - ("private_compact_birth_datetime", re.compile(r"REDACTED_DATE[_-]?REDACTED_TIME")), - ("private_slug_birth_date", re.compile(r"REDACTED_YEAR[_-]04[_-]17")), - ("private_birth_time_literal", re.compile(r"REDACTED_YEAR.{0,120}\bREDACTED_TIME\b|\bREDACTED_TIME\b.{0,120}REDACTED_YEAR|REDACTED_TIME")), - ("private_place_han", re.compile(r"REDACTED_PLACE|REDACTED_PLACE|REDACTED_HOSPITAL")), - ("private_case_slug", re.compile(r"user_REDACTED_YEAR|redacted_place", re.IGNORECASE)), - ( - "private_birth_dict_tuple", - re.compile( - r"(?s)(?:year|--year|datetime\()\D*REDACTED_YEAR.{0,220}" - r"(?:month|--month|,\s*)\D*4.{0,220}" - r"(?:day|--day|,\s*)\D*17.{0,220}" - r"(?:hour|--hour|,\s*)\D*14.{0,220}" - r"(?:minute|--minute|,\s*)\D*49" - ), - ), -] +LOCAL_DENY_PATTERN_FILE = Path("scratch/local/public_release_deny_patterns.txt") +ENV_DENY_PATTERNS = "PUBLIC_RELEASE_DENY_PATTERNS" -def _is_skipped(path: Path) -> bool: - rel = path.relative_to(ROOT).as_posix() +def _literal_patterns(values: Iterable[str], prefix: str) -> list[tuple[str, re.Pattern[str]]]: + patterns: list[tuple[str, re.Pattern[str]]] = [] + for index, value in enumerate(values, start=1): + value = value.strip() + if not value or value.startswith("#"): + continue + patterns.append((f"{prefix}_{index:02d}", re.compile(re.escape(value), re.IGNORECASE))) + return patterns + + +def deny_patterns(root: Path = ROOT) -> list[tuple[str, re.Pattern[str]]]: + patterns = _literal_patterns(os.environ.get(ENV_DENY_PATTERNS, "").splitlines(), "private_env_pattern") + local_file = root / LOCAL_DENY_PATTERN_FILE + if local_file.is_file(): + patterns.extend( + _literal_patterns( + local_file.read_text(encoding="utf-8", errors="ignore").splitlines(), + "private_local_pattern", + ) + ) + return patterns + + +def _is_skipped(path: Path, root: Path = ROOT) -> bool: + try: + rel = path.relative_to(root).as_posix() + except ValueError: + rel = path.as_posix() if rel in SKIP_FILES: return True return any(rel == item or rel.startswith(f"{item}/") for item in SKIP_DIRS) @@ -73,23 +84,30 @@ def iter_release_files(root: Path = ROOT) -> Iterable[Path]: cwd=root, text=True, capture_output=True, - check=True, + check=False, ) - for line in completed.stdout.splitlines(): - path = root / line - if not path.is_file() or _is_skipped(path): + if completed.returncode == 0: + candidates = (root / line for line in completed.stdout.splitlines()) + else: + candidates = (path for path in root.rglob("*") if path.is_file()) + for path in candidates: + if not path.is_file() or _is_skipped(path, root): continue if path.suffix.lower() in TEXT_SUFFIXES: yield path -def scan_text(path: Path, text: str) -> list[dict[str, object]]: +def scan_text( + path: Path, + text: str, + patterns: Iterable[tuple[str, re.Pattern[str]]] | None = None, +) -> list[dict[str, object]]: findings: list[dict[str, object]] = [] try: display_path = path.relative_to(ROOT).as_posix() except ValueError: display_path = path.as_posix() - for rule_id, pattern in DENY_PATTERNS: + for rule_id, pattern in (patterns if patterns is not None else deny_patterns()): for match in pattern.finditer(text): line = text.count("\n", 0, match.start()) + 1 findings.append( @@ -105,13 +123,14 @@ def scan_text(path: Path, text: str) -> list[dict[str, object]]: def build_report(root: Path = ROOT) -> dict[str, object]: findings: list[dict[str, object]] = [] scanned = 0 + patterns = deny_patterns(root) for path in iter_release_files(root): try: text = path.read_text(encoding="utf-8") except UnicodeDecodeError: text = path.read_text(encoding="utf-8", errors="ignore") scanned += 1 - findings.extend(scan_text(path, text)) + findings.extend(scan_text(path, text, patterns)) return { "scope": "public_release_privacy_scan", "scanned_files": scanned, diff --git a/scripts/real_case_replay_validator.py b/scripts/real_case_replay_validator.py new file mode 100644 index 00000000..b48fb509 --- /dev/null +++ b/scripts/real_case_replay_validator.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Validate structured real-case outcome replay manifests.""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path +from typing import Any + +CASE_REQUIRED_FIELDS = { + "case_id", + "source", + "chart_signature", + "event_outcomes", + "similarity", + "replay", +} +SOURCE_REQUIRED_FIELDS = {"url", "source_grade", "license_or_quote_boundary"} +EVENT_REQUIRED_FIELDS = {"event_type", "event_date", "outcome"} + + +def _missing(mapping: dict[str, Any], required: set[str]) -> list[str]: + return sorted(name for name in required if name not in mapping) + + +def _case_errors(case: Any, index: int) -> list[dict[str, Any]]: + if not isinstance(case, dict): + return [{"case_index": index, "field": "case", "error": "not_object"}] + + errors: list[dict[str, Any]] = [] + for field in _missing(case, CASE_REQUIRED_FIELDS): + errors.append({"case_id": case.get("case_id"), "field": field, "error": "missing"}) + + source = case.get("source") + if isinstance(source, dict): + for field in _missing(source, SOURCE_REQUIRED_FIELDS): + errors.append({"case_id": case.get("case_id"), "field": f"source.{field}", "error": "missing"}) + elif "source" in case: + errors.append({"case_id": case.get("case_id"), "field": "source", "error": "not_object"}) + + events = case.get("event_outcomes") + if isinstance(events, list): + if not events: + errors.append({"case_id": case.get("case_id"), "field": "event_outcomes", "error": "empty"}) + for event_index, event in enumerate(events): + if not isinstance(event, dict): + errors.append({"case_id": case.get("case_id"), "field": f"event_outcomes[{event_index}]", "error": "not_object"}) + continue + for field in _missing(event, EVENT_REQUIRED_FIELDS): + errors.append({"case_id": case.get("case_id"), "field": f"event_outcomes[{event_index}].{field}", "error": "missing"}) + elif "event_outcomes" in case: + errors.append({"case_id": case.get("case_id"), "field": "event_outcomes", "error": "not_array"}) + + return errors + + +def validate_manifest(path: str | Path) -> dict[str, Any]: + manifest_path = Path(path) + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + cases = manifest.get("cases") + if not isinstance(cases, list): + return { + "status": "blocked", + "manifest_path": str(manifest_path), + "case_count": 0, + "replay_ready_count": 0, + "blocked_reason": "cases_not_array", + "errors": [{"field": "cases", "error": "not_array"}], + } + + errors: list[dict[str, Any]] = [] + replay_ready_count = 0 + for index, case in enumerate(cases): + case_errors = _case_errors(case, index) + errors.extend(case_errors) + replay = case.get("replay") if isinstance(case, dict) else {} + if not case_errors and isinstance(replay, dict) and replay.get("outcome_replay_status") == "replayed": + replay_ready_count += 1 + + if errors: + status = "invalid" + blocked_reason = "case_contract_errors" + elif not cases: + status = "blocked" + blocked_reason = manifest.get("blocked_reason") or "no_structured_outcome_replay_cases_imported" + elif replay_ready_count == len(cases): + status = "pass" + blocked_reason = None + else: + status = "partial" + blocked_reason = "some_cases_not_replayed" + + return { + "status": status, + "manifest_path": str(manifest_path), + "schema_version": manifest.get("schema_version"), + "case_schema": manifest.get("case_schema"), + "case_count": len(cases), + "replay_ready_count": replay_ready_count, + "blocked_reason": blocked_reason, + "errors": errors, + "runtime_boundary": manifest.get("runtime_boundary", ""), + } + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("manifest", nargs="?", default="references/real_case_calibration/replay_manifest.json") + args = parser.parse_args(argv) + print(json.dumps(validate_manifest(args.manifest), ensure_ascii=False, indent=2, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/skill_release_package.py b/scripts/skill_release_package.py index bc8c3a81..d803df43 100644 --- a/scripts/skill_release_package.py +++ b/scripts/skill_release_package.py @@ -24,11 +24,14 @@ SKIP_NAMES = {".env", ".env.local"} GENERATED_DOCS = { "INSTALL.md": """# Jyotish Skill Install +Basic Git version: https://github.com/732642856/yinduzhanxing + 1. Unzip this package into a local folder. 2. Run `python3 scripts/public_release_privacy_scan.py`. 3. Run `python3 scripts/user_invocation_acceptance_check.py`. -4. If the check passes, load this folder as a Codex skill/plugin or call `mcp_server.py`. -5. VedAstro cloud evidence is optional. Without `official_raw_response`, reports must say `official_blocked` or `local_fallback`. +4. Run `python3 scripts/skill_release_package.py --edition premium_cloud_drive`. +5. If the checks pass, load this folder as a Codex skill/plugin or call `mcp_server.py`. +6. VedAstro cloud evidence is optional. Without `official_raw_response`, reports must say `official_blocked` or `local_fallback`. Do not add private birth data, API keys, or desktop oracle screenshots to this package. """, @@ -43,6 +46,11 @@ Do not add private birth data, API keys, or desktop oracle screenshots to this p 请使用 strict_workflow,并在输出中标明 VedAstro / PyJHora-JHora / jyotishganit / Real Case Calibration 的状态。 如果没有 VedAstro official_raw_response,请标记 official_blocked 或 local_fallback。 +如果我提供西方占星导出,请作为 western_oracle_payload 进入统一主链,不要把单边西占信号说成双系统互证。 + +## Highest Quality Mode + +请走高级版最高品质流程:direct_chart 或 rectification 入口 → evidence_packet → Technique Audit Table → VedAstro 三态 → 三引擎 parity replay 状态 → Real Case outcome replay 状态 → guided_topics。 ## Birth-Time Rectification @@ -50,6 +58,14 @@ Do not add private birth data, API keys, or desktop oracle screenshots to this p """, } +REQUIRED_CONTRACTS = [ + "references/real_case_calibration/replay_manifest.json", + "references/oracle/three_engine_parity_replay_manifest.json", + "references/oracle/western_oracle_adapter_contract.md", + "scripts/user_invocation_acceptance_check.py", + "scripts/diagnose_external_engine_adapters.py", +] + def _git_files() -> list[str]: completed = subprocess.run(["git", "ls-files", "-z"], cwd=ROOT, capture_output=True, check=True) @@ -70,15 +86,49 @@ def _edition_files(edition: str) -> list[str]: if edition not in manifest["editions"]: raise ValueError(f"unknown edition: {edition}") files = _git_files() + files = sorted(set(files) | {path for path in REQUIRED_CONTRACTS if (ROOT / path).is_file()}) if edition == "basic_git": keep = ("SKILL.md", "README.md", "mcp_server.py", ".codex-plugin/", "scripts/", "tests/") files = [path for path in files if path in keep or any(path.startswith(prefix) for prefix in keep if prefix.endswith("/"))] return [path for path in files if _allowed(path)] +def _required_contracts() -> dict[str, bool]: + return {path: (ROOT / path).is_file() for path in REQUIRED_CONTRACTS} + + +def _package_acceptance(edition: str, privacy: dict[str, Any], files: list[str]) -> dict[str, Any]: + contracts = _required_contracts() + return { + "scope": "skill_release_package_acceptance", + "schema_version": 1, + "edition": edition, + "status": "pass" if privacy["status"] == "pass" and all(contracts.values()) else "blocked", + "privacy_scan_status": privacy["status"], + "file_count": len(files), + "required_contracts": contracts, + "acceptance_commands": release_manifest()["acceptance_commands"], + "boundary": ( + "Package acceptance proves release hygiene and contracts only; it does not prove external oracle raw " + "closure or same-chart parity until replay manifests contain imported rows." + ), + } + + +def _generated_docs(edition: str, privacy: dict[str, Any], files: list[str]) -> dict[str, str]: + manifest = release_manifest() + acceptance = _package_acceptance(edition, privacy, files) + docs = dict(GENERATED_DOCS) + docs["RELEASE_MANIFEST.json"] = json.dumps(manifest, ensure_ascii=False, indent=2, sort_keys=True) + "\n" + docs["PACKAGE_ACCEPTANCE.json"] = json.dumps(acceptance, ensure_ascii=False, indent=2, sort_keys=True) + "\n" + return docs + + def build_package_plan(edition: str = "premium_cloud_drive") -> dict[str, Any]: privacy = privacy_scan() files = _edition_files(edition) + generated_docs = _generated_docs(edition, privacy, files) + acceptance = _package_acceptance(edition, privacy, files) return { "scope": "skill_release_package", "schema_version": 1, @@ -87,7 +137,9 @@ def build_package_plan(edition: str = "premium_cloud_drive") -> dict[str, Any]: "privacy_scan_status": privacy["status"], "file_count": len(files), "files": files, - "generated_files": sorted(GENERATED_DOCS), + "generated_files": sorted(generated_docs), + "required_contracts": acceptance["required_contracts"], + "package_acceptance_status": acceptance["status"], "boundary": "Dry-run plan only; use --write-zip to create a local zip, then upload manually if desired.", } @@ -100,7 +152,8 @@ def write_zip(edition: str, output: Path) -> dict[str, Any]: with zipfile.ZipFile(output, "w", compression=zipfile.ZIP_DEFLATED) as archive: for rel in plan["files"]: archive.write(ROOT / rel, rel) - for rel, text in GENERATED_DOCS.items(): + privacy = privacy_scan() + for rel, text in _generated_docs(edition, privacy, plan["files"]).items(): archive.writestr(rel, text) return {**plan, "mode": "write_zip", "zip_path": str(output)} diff --git a/scripts/three_engine_parity_replay_validator.py b/scripts/three_engine_parity_replay_validator.py new file mode 100644 index 00000000..5d3a9ba5 --- /dev/null +++ b/scripts/three_engine_parity_replay_validator.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Validate same-chart three-engine parity replay manifests.""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path +from typing import Any + +REQUIRED_ENGINES = {"VedAstro", "PyJHora_JHora", "jyotishganit"} +REQUIRED_ROW_FIELDS = {"section", "field", "local_value", "oracle_values", "status"} +VALID_ROW_STATUSES = {"match", "mismatch", "blocked", "not_comparable"} + + +def _row_errors(row: Any, index: int) -> list[dict[str, Any]]: + if not isinstance(row, dict): + return [{"row_index": index, "field": "row", "error": "not_object"}] + errors: list[dict[str, Any]] = [] + for field in sorted(REQUIRED_ROW_FIELDS - set(row)): + errors.append({"row_index": index, "field": field, "error": "missing"}) + if row.get("status") not in VALID_ROW_STATUSES: + errors.append({"row_index": index, "field": "status", "error": "invalid"}) + if "oracle_values" in row and not isinstance(row.get("oracle_values"), dict): + errors.append({"row_index": index, "field": "oracle_values", "error": "not_object"}) + return errors + + +def validate_manifest(path: str | Path) -> dict[str, Any]: + manifest_path = Path(path) + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + engines = manifest.get("engines") if isinstance(manifest.get("engines"), dict) else {} + rows = manifest.get("comparison_rows") + errors: list[dict[str, Any]] = [] + + missing_engines = sorted(REQUIRED_ENGINES - set(engines)) + for engine in missing_engines: + errors.append({"field": f"engines.{engine}", "error": "missing"}) + + if not isinstance(rows, list): + rows = [] + errors.append({"field": "comparison_rows", "error": "not_array"}) + + for index, row in enumerate(rows): + errors.extend(_row_errors(row, index)) + + counts = {"match": 0, "mismatch": 0, "blocked": 0, "not_comparable": 0} + for row in rows: + if isinstance(row, dict) and row.get("status") in counts: + counts[row["status"]] += 1 + + if errors: + status = "invalid" + blocked_reason = "parity_manifest_contract_errors" + elif not rows: + status = "blocked" + blocked_reason = manifest.get("blocked_reason") or "no_same_chart_oracle_rows_imported" + elif counts["mismatch"]: + status = "mismatch" + blocked_reason = None + elif counts["blocked"]: + status = "partial" + blocked_reason = "some_comparison_rows_blocked" + else: + status = "pass" + blocked_reason = None + + return { + "status": status, + "tested": status in {"pass", "mismatch", "partial"}, + "manifest_path": str(manifest_path), + "case_id": manifest.get("case_id"), + "birth_data_policy": manifest.get("birth_data_policy"), + "engine_count": len(engines), + "required_engines": sorted(REQUIRED_ENGINES), + "missing_engines": missing_engines, + "comparison_row_count": len(rows), + "match_count": counts["match"], + "mismatch_count": counts["mismatch"], + "blocked_row_count": counts["blocked"], + "not_comparable_count": counts["not_comparable"], + "blocked_reason": blocked_reason, + "errors": errors, + "runtime_boundary": manifest.get("runtime_boundary", ""), + } + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("manifest", nargs="?", default="references/oracle/three_engine_parity_replay_manifest.json") + args = parser.parse_args(argv) + print(json.dumps(validate_manifest(args.manifest), ensure_ascii=False, indent=2, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/unified_consultation_orchestrator.py b/scripts/unified_consultation_orchestrator.py index 988817af..c3d85c0b 100644 --- a/scripts/unified_consultation_orchestrator.py +++ b/scripts/unified_consultation_orchestrator.py @@ -18,11 +18,21 @@ try: except Exception: # pragma: no cover - import path varies in tests/CLI from scripts.diagnose_jyotishganit_adapter import build_report as build_jyotishganit_adapter_report +try: + from cross_system_arbitrator import build_cross_system_arbitration +except Exception: # pragma: no cover - import path varies in tests/CLI + from scripts.cross_system_arbitrator import build_cross_system_arbitration + try: from functional_benefics import derive_functional_benefic_malefic except Exception: # pragma: no cover - import path varies in tests/CLI from scripts.functional_benefics import derive_functional_benefic_malefic +try: + from real_case_replay_validator import validate_manifest as validate_real_case_replay_manifest +except Exception: # pragma: no cover - import path varies in tests/CLI + from scripts.real_case_replay_validator import validate_manifest as validate_real_case_replay_manifest + @dataclass(frozen=True) class RouteDefinition: @@ -426,12 +436,16 @@ class UnifiedConsultationOrchestrator: ), } missing = [name for name, section in sections.items() if section.get("status") == "missing"] + signals = chart_data.get("cross_system_signals") + if not isinstance(signals, list): + signals = modules.get("cross_system_signals") if isinstance(modules.get("cross_system_signals"), list) else [] return { "status": "complete" if not missing else "partial", "route": dict(route_packet), "required_sections": list(self.EVIDENCE_PACKET_REQUIRED_SECTIONS), "sections": sections, "functional_benefic_malefic": functional_layer, + "signals": [item for item in signals if isinstance(item, dict)], "missing_sections": missing, } @@ -463,6 +477,8 @@ class UnifiedConsultationOrchestrator: "event_trigger_keywords": ["UL", "Darapada", "7th lord", "DK"], }, } + replay_manifest_path = Path(__file__).resolve().parents[1] / "references/real_case_calibration/replay_manifest.json" + replay_manifest = validate_real_case_replay_manifest(replay_manifest_path) candidate_refs = case_index_by_domain.get(route, []) packet = machine_evidence_packet if isinstance(machine_evidence_packet, dict) else {} sections = packet.get("sections") if isinstance(packet.get("sections"), dict) else {} @@ -518,6 +534,8 @@ class UnifiedConsultationOrchestrator: "route": route, "source_roots": ["references/real_case_studies", "docs/benchmark"], "case_index_by_domain": case_index_by_domain, + "required_replay_schema": "references/real_case_calibration/catalog.schema.json", + "outcome_replay_manifest": replay_manifest, "candidate_refs": list(candidate_refs), "scored_candidates": scored_candidates, "reference_grade": scored_candidates[0]["reference_grade"] if scored_candidates else "ungraded_until_similarity_scored", @@ -539,6 +557,7 @@ class UnifiedConsultationOrchestrator: interpretation_source_runtime_coverage: dict[str, Any] | None = None, machine_evidence_packet: dict[str, Any] | None = None, real_case_calibration: dict[str, Any] | None = None, + western_evidence_packet: dict[str, Any] | None = None, blind: bool = False, ) -> dict[str, Any]: official = vedastro_official if isinstance(vedastro_official, dict) else {} @@ -575,6 +594,13 @@ class UnifiedConsultationOrchestrator: blocked_items.append("real_case_calibration_not_yet_materialized") elif case_status != "complete": blocked_items.append("real_case_calibration_partial") + cross_system_arbitration = build_cross_system_arbitration( + route_packet=route_packet, + jyotish_evidence=packet, + western_evidence=western_evidence_packet, + ) + if cross_system_arbitration["status"] != "used": + blocked_items.append("cross_system_arbitration_not_complete") technique_audit_table = [ { "technique": "VedAstro Cloud State", @@ -606,6 +632,7 @@ class UnifiedConsultationOrchestrator: else "claims_capped_until_pyjhora_jhora_jyotishganit_are_invoked_for_this_run" ), }, + *cross_system_arbitration["technique_audit_rows"], { "technique": "Evidence Packet", "status": packet_status, @@ -653,6 +680,7 @@ class UnifiedConsultationOrchestrator: "vedastro_cloud_state": vedastro_state, "vedastro_runtime_truth": dict(runtime_truth), "external_engine_cross_validation": external_cross_validation, + "cross_system_arbitration": cross_system_arbitration, "source_priority": { "mode": self.SOURCE_PRIORITY["mode"], "priority": list(self.SOURCE_PRIORITY["priority"]), @@ -691,7 +719,10 @@ class UnifiedConsultationOrchestrator: "technique_audit_table": technique_audit_table, "required_rows": [ "VedAstro Cloud State", + "VedAstro Raw Archive Manifest", "External Engine Cross-Validation", + "Western Cross-Validation", + "Cross-System Arbitration", "Evidence Packet", "Blind Technical Mode", "MEVG / Global Web Evidence", diff --git a/scripts/user_invocation_acceptance_check.py b/scripts/user_invocation_acceptance_check.py index de00f9b0..33ba7182 100644 --- a/scripts/user_invocation_acceptance_check.py +++ b/scripts/user_invocation_acceptance_check.py @@ -158,7 +158,8 @@ def main() -> int: if cursor in (None, [], ""): errors.append(f"missing_entrypoint_{'_'.join(path)}") - if adapter_report.get("status") not in {"pass", "partial"}: + usable_adapter_statuses = {"pass", "partial", "complete"} + if adapter_report.get("status") not in usable_adapter_statuses: errors.append("external_adapter_status_unusable") result = { @@ -168,7 +169,7 @@ def main() -> int: "checks": { "user_invocation_tests": pytest_check["returncode"] == 0, "guided_topics_entrypoint": entrypoint["returncode"] == 0 and not any(e.startswith("missing_entrypoint_") for e in errors), - "external_adapter_diagnostics": adapter_report.get("status") in {"pass", "partial"}, + "external_adapter_diagnostics": adapter_report.get("status") in usable_adapter_statuses, }, "entrypoint_runtime_mode": _runtime_summary(entrypoint_report), "entrypoint_strict_workflow": _strict_summary(entrypoint_report), diff --git a/scripts/western_evidence_packet.py b/scripts/western_evidence_packet.py new file mode 100644 index 00000000..65108a6f --- /dev/null +++ b/scripts/western_evidence_packet.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Western astrology evidence packet contract.""" + +from __future__ import annotations + +from typing import Any + + +REQUIRED_SECTIONS = [ + "natal", + "timing_techniques", + "signals", +] + + +def _section(value: Any, source_path: str) -> dict[str, Any]: + present = bool(value) + return { + "status": "used" if present else "missing", + "source_path": source_path, + } + + +def _normalize_signals(signals: list[dict[str, Any]] | None) -> list[dict[str, Any]]: + normalized: list[dict[str, Any]] = [] + for item in signals or []: + if not isinstance(item, dict) or not item.get("theme") or not item.get("claim"): + continue + normalized.append( + { + "theme": str(item.get("theme")), + "claim": str(item.get("claim")), + "timing": str(item.get("timing") or "unspecified"), + "source": str(item.get("source") or "western_evidence_packet"), + } + ) + return normalized + + +def build_western_evidence_packet( + *, + route_packet: dict[str, Any], + natal: dict[str, Any] | None, + timing_techniques: dict[str, Any] | None, + signals: list[dict[str, Any]] | None, +) -> dict[str, Any]: + """Materialize a small auditable Western evidence packet.""" + + normalized_signals = _normalize_signals(signals) + sections = { + "natal": _section(natal, "western.natal"), + "timing_techniques": _section(timing_techniques, "western.timing_techniques"), + "signals": _section(normalized_signals, "western.signals"), + } + missing = [name for name in REQUIRED_SECTIONS if sections[name]["status"] == "missing"] + return { + "system": "western_astrology", + "status": "complete" if not missing else "partial", + "route": dict(route_packet), + "required_sections": list(REQUIRED_SECTIONS), + "sections": sections, + "natal": natal or {}, + "timing_techniques": timing_techniques or {}, + "signals": normalized_signals, + "missing_sections": missing, + "license_boundary": ( + "This packet stores derived evidence only. Kerykeion, pyswisseph, Flatlib, Immanuel, or desktop " + "software outputs must remain external oracle inputs unless their licenses permit bundling." + ), + } diff --git a/scripts/western_oracle_adapter.py b/scripts/western_oracle_adapter.py new file mode 100644 index 00000000..93e1fc59 --- /dev/null +++ b/scripts/western_oracle_adapter.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Normalize external Western astrology oracle JSON into a western evidence packet.""" + +from __future__ import annotations + +import argparse +import json +import sys +from pathlib import Path +from typing import Any + +try: + from western_evidence_packet import build_western_evidence_packet +except Exception: # pragma: no cover - import path varies in tests/CLI + from scripts.western_evidence_packet import build_western_evidence_packet + + +_ASPECT_ALIASES = { + "conj": "conjunction", + "conjunction": "conjunction", + "trine": "trine", + "sextile": "sextile", + "square": "square", + "opposition": "opposition", + "opp": "opposition", +} + + +def _norm(value: Any) -> str: + return str(value or "").strip().lower().replace("-", "_").replace(" ", "_") + + +def _aspect_label(item: dict[str, Any]) -> tuple[str, str, str]: + planet = _norm(item.get("planet") or item.get("transit_planet")) + aspect = _ASPECT_ALIASES.get(_norm(item.get("aspect")), _norm(item.get("aspect"))) + target = _norm(item.get("target") or item.get("point") or item.get("natal_point")) + return planet, aspect, target + + +def _timing(item: dict[str, Any]) -> str: + return str(item.get("timing") or item.get("date") or item.get("window") or "unspecified") + + +def _signals_from_aspects(aspects: list[dict[str, Any]]) -> list[dict[str, Any]]: + signals: list[dict[str, Any]] = [] + for item in aspects: + if not isinstance(item, dict): + continue + planet, aspect, target = _aspect_label(item) + timing = _timing(item) + source = str(item.get("source") or f"{planet} {aspect} {target}").replace("_", " ") + if planet == "uranus" and aspect == "conjunction" and target in {"mc", "midheaven"}: + signals.append( + { + "theme": "career_relocation", + "claim": "career_triggered_relocation", + "timing": timing, + "source": source, + } + ) + if planet == "jupiter" and aspect in {"trine", "sextile"} and target in {"mercury", "venus", "mc", "midheaven"}: + signals.append( + { + "theme": "career", + "claim": "client_cooperation_opportunity", + "timing": timing, + "source": source, + } + ) + if planet == "saturn" and aspect == "conjunction" and target in {"sun", "mercury", "venus"}: + signals.append( + { + "theme": "career", + "claim": "career_responsibility_test", + "timing": timing, + "source": source, + } + ) + return signals + + +def build_packet_from_oracle_payload( + payload: dict[str, Any], + *, + route_packet: dict[str, Any], +) -> dict[str, Any]: + """Build a standard Western packet from an external-oracle JSON payload.""" + + explicit_signals = payload.get("signals") if isinstance(payload.get("signals"), list) else [] + aspects = payload.get("aspects") if isinstance(payload.get("aspects"), list) else [] + signals = list(explicit_signals) or _signals_from_aspects(aspects) + timing_techniques = payload.get("timing_techniques") + if not isinstance(timing_techniques, dict): + timing_techniques = {} + if aspects and "aspects" not in timing_techniques: + timing_techniques = {**timing_techniques, "aspects": aspects} + packet = build_western_evidence_packet( + route_packet=route_packet, + natal=payload.get("natal") if isinstance(payload.get("natal"), dict) else {}, + timing_techniques=timing_techniques, + signals=signals, + ) + packet["source_engine"] = str(payload.get("source_engine") or payload.get("engine") or "external_western_oracle_json") + packet["adapter_boundary"] = { + "bundles_external_code": False, + "accepted_input": "external_json_export", + "role": "western_cross_validation_evidence", + } + return packet + + +def _parse_args(argv: list[str]) -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--input", required=True, help="Path to external Western oracle JSON export.") + parser.add_argument("--theme", default="general", help="Primary theme, e.g. career/marriage/wealth.") + parser.add_argument("--question-type", default=None, help="Question type override.") + return parser.parse_args(argv) + + +def main(argv: list[str] | None = None) -> int: + args = _parse_args(list(argv or sys.argv[1:])) + payload = json.loads(Path(args.input).read_text(encoding="utf-8")) + route_packet = { + "question_type": args.question_type or args.theme, + "primary_theme": args.theme, + } + packet = build_packet_from_oracle_payload(payload, route_packet=route_packet) + print(json.dumps(packet, ensure_ascii=False, indent=2, sort_keys=True)) + return 0 + + +if __name__ == "__main__": # pragma: no cover + raise SystemExit(main()) diff --git a/tests/test_api_server_security.py b/tests/test_api_server_security.py index 4bbc95ce..f0325d00 100644 --- a/tests/test_api_server_security.py +++ b/tests/test_api_server_security.py @@ -2327,6 +2327,66 @@ def test_consultation_workflow_uses_unified_orchestrator_contract(monkeypatch) - assert result['chart']['special_lagnas']['precision'] == 'sunrise_correct' +def test_consultation_workflow_accepts_western_oracle_payload(monkeypatch) -> None: + handler = _handler() + fake_chart = { + 'success': True, + 'birth_info': {'date': '1955-02-24', 'time': '19:15', 'tz': 8}, + 'ascendant': {'lon': 92.0, 'sign': 'Cancer'}, + 'planets': _sample_planets(), + 'chart': { + 'ascendant': {'lon': 92.0, 'sign': 'Cancer'}, + 'planets': _sample_planets(), + }, + 'modules': {}, + 'cross_system_signals': [ + { + 'theme': 'career_relocation', + 'claim': 'career_triggered_relocation', + 'timing': '2026-07', + 'source': 'jyotish_runtime_signal', + } + ], + } + + monkeypatch.setattr(handler, '_compute_chart', lambda body: fake_chart) + monkeypatch.setattr(handler, '_compute_rectification_gate', lambda body: {'endpoint': 'rectification_gate'}) + monkeypatch.setattr(handler, '_compute_thematic_report', lambda body: {'endpoint': 'thematic_report'}) + + result = handler._compute_consultation_workflow({ + 'entry_mode': 'direct_chart', + 'question': 'career relocation timing', + 'year': 1955, + 'month': 2, + 'day': 24, + 'hour': 19, + 'minute': 15, + 'lat': 37.7749, + 'lon': -122.4194, + 'tz': 8, + 'theme': ['career'], + 'western_oracle_payload': { + 'source_engine': 'kerykeion_external_json', + 'natal': {'ascendant': 'Virgo', 'mc': 'Gemini'}, + 'timing_techniques': {'transits': [{'date': '2026-07'}]}, + 'aspects': [ + { + 'date': '2026-07', + 'planet': 'Uranus', + 'aspect': 'conjunction', + 'target': 'MC', + } + ], + }, + }) + + assert result['western_evidence_packet']['source_engine'] == 'kerykeion_external_json' + assert result['runtime_evidence_log']['cross_system_arbitration']['status'] == 'used' + assert result['runtime_evidence_log']['cross_system_arbitration']['shared_signals'][0]['claim'] == 'career_triggered_relocation' + audit = result['runtime_evidence_log']['quality_gate']['technique_audit_table'] + assert any(row['technique'] == 'Western Cross-Validation' and row['used'] is True for row in audit) + + def test_consultation_workflow_reuses_chart_data_for_thematic_report_without_recursive_full_reading(monkeypatch) -> None: handler = _handler() fake_chart = { diff --git a/tests/test_cross_system_arbitrator.py b/tests/test_cross_system_arbitrator.py new file mode 100644 index 00000000..793065a8 --- /dev/null +++ b/tests/test_cross_system_arbitrator.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +"""Cross-system Jyotish/Western arbitration contract tests.""" + +from __future__ import annotations + +from scripts.cross_system_arbitrator import build_cross_system_arbitration + + +def test_cross_system_arbitrator_promotes_shared_timed_signal() -> None: + packet = build_cross_system_arbitration( + route_packet={"question_type": "career", "primary_theme": "career"}, + jyotish_evidence={ + "status": "complete", + "signals": [ + { + "theme": "career_relocation", + "claim": "career_triggered_relocation", + "timing": "2026-08-24..2026-09-28", + "source": "Saturn/Ketu + Rahu 4H/10H axis", + } + ], + }, + western_evidence={ + "status": "complete", + "signals": [ + { + "theme": "career_relocation", + "claim": "career_triggered_relocation", + "timing": "2026-08-24..2026-09-28", + "source": "Uranus conjunct MC opposite IC", + } + ], + }, + ) + + assert packet["status"] == "used" + assert packet["primary_theme"] == "career" + assert packet["shared_signals"][0]["theme"] == "career_relocation" + assert packet["shared_signals"][0]["claim"] == "career_triggered_relocation" + assert packet["shared_signals"][0]["timing"] == "2026-08-24..2026-09-28" + assert packet["shared_signals"][0]["confidence_effect"] == "raises_confidence" + assert packet["conflicts"] == [] + + +def test_cross_system_arbitrator_blocks_without_western_packet() -> None: + packet = build_cross_system_arbitration( + route_packet={"question_type": "career", "primary_theme": "career"}, + jyotish_evidence={"status": "partial", "signals": []}, + western_evidence=None, + ) + + assert packet["status"] == "blocked" + assert packet["western_cross_validation"]["status"] == "blocked" + assert "western_evidence_packet_missing" in packet["blocked_items"] + + +def test_cross_system_arbitrator_marks_conflicting_claims() -> None: + packet = build_cross_system_arbitration( + route_packet={"question_type": "career", "primary_theme": "career"}, + jyotish_evidence={ + "status": "complete", + "signals": [{"theme": "career", "claim": "stable_internal_role", "timing": "2026-07"}], + }, + western_evidence={ + "status": "complete", + "signals": [{"theme": "career", "claim": "external_project_pivot", "timing": "2026-07"}], + }, + ) + + assert packet["status"] == "conflict" + assert packet["conflicts"][0]["theme"] == "career" + assert packet["conflicts"][0]["confidence_effect"] == "lowers_confidence" diff --git a/tests/test_external_engine_adapter_diagnostics.py b/tests/test_external_engine_adapter_diagnostics.py index a5e48e81..3288df1e 100644 --- a/tests/test_external_engine_adapter_diagnostics.py +++ b/tests/test_external_engine_adapter_diagnostics.py @@ -37,4 +37,6 @@ def test_external_engine_adapter_diagnostics_aggregates_three_engines() -> None: assert "raw_output_path" in contract["expected_oracle_fields"]["PyJHora/JHora"] assert contract["engine_states"]["jyotishganit"]["available"] is True assert contract["engine_states"]["PyJHora/JHora"]["tested"] is False + assert contract["replay_manifest"]["tested"] is False + assert contract["replay_manifest"]["blocked_reason"] == "no_same_chart_oracle_rows_imported" assert report["status"] in {"complete", "partial"} diff --git a/tests/test_mcp_strict_workflow_career.py b/tests/test_mcp_strict_workflow_career.py index a620c296..402c2508 100644 --- a/tests/test_mcp_strict_workflow_career.py +++ b/tests/test_mcp_strict_workflow_career.py @@ -186,6 +186,74 @@ def test_mcp_strict_workflow_returns_runtime_evidence_log(monkeypatch) -> None: assert result["runtime_evidence_log"]["quality_gate"]["technique_audit_table"][1]["technique"] == "VedAstro Raw Archive Manifest" +def test_mcp_strict_workflow_preserves_western_evidence_packet(monkeypatch) -> None: + seen = {} + + def fake_execute(**kwargs): + seen.update(kwargs) + return { + "chart": { + "modules": {}, + "cross_system_signals": [ + { + "theme": "career_relocation", + "claim": "career_triggered_relocation", + "timing": "2026-07", + "source": "jyotish_runtime_signal", + } + ], + "ai_prompt_pack": { + "evidence_snapshot": { + "vedastro_official_snapshot": { + "status": "ok", + "official_primary_evidence": {"chart_core": {"status": "ok"}}, + } + } + }, + }, + "routing": {"question_type": "career", "primary_theme": "career"}, + "entry_mode": "direct_chart", + "runtime_planner": {"executed_steps": ["compute_chart"], "skipped_steps": []}, + "western_evidence_packet": kwargs["western_evidence_packet"], + } + + western_packet = { + "system": "western_astrology", + "status": "complete", + "signals": [ + { + "theme": "career_relocation", + "claim": "career_triggered_relocation", + "timing": "2026-07", + "source": "western_oracle_signal", + } + ], + } + + monkeypatch.setattr(mcp_server, "_execute_mcp_consultation_workflow", fake_execute) + monkeypatch.setattr(mcp_server, "_maybe_attach_vedastro_evidence", lambda route, chart, **kwargs: chart) + monkeypatch.setattr(mcp_server, "_collect_strict_evidence", lambda route, chart: {"question_type": route}) + + result = mcp_server.strict_workflow( + question="career relocation timing", + year=1955, + month=2, + day=24, + hour=19, + minute=15, + lat=37.7749, + lon=-122.4194, + tz=8, + age=33, + transit_date="2026-07-05", + western_evidence_packet=western_packet, + ) + + assert seen["western_evidence_packet"] == western_packet + assert result["runtime_evidence_log"]["cross_system_arbitration"]["status"] == "used" + assert result["runtime_evidence_log"]["cross_system_arbitration"]["shared_signals"][0]["claim"] == "career_triggered_relocation" + + def test_career_blocks_label_when_d10_is_missing_but_preserves_jaimini_context() -> None: result = _base_career_result() del result["modules"]["varga_full"]["D10_Dasamsa"] diff --git a/tests/test_public_release_privacy_scan.py b/tests/test_public_release_privacy_scan.py index 56367bab..24f9d15d 100644 --- a/tests/test_public_release_privacy_scan.py +++ b/tests/test_public_release_privacy_scan.py @@ -2,20 +2,30 @@ from __future__ import annotations from pathlib import Path -from scripts.public_release_privacy_scan import build_report, scan_text +from scripts.public_release_privacy_scan import build_report, deny_patterns, iter_release_files, scan_text -def test_public_release_privacy_scan_finds_private_birth_tuple() -> None: +def test_public_release_privacy_scan_finds_private_configured_pattern(monkeypatch) -> None: + monkeypatch.setenv("PUBLIC_RELEASE_DENY_PATTERNS", "SECRET_BIRTH_TOKEN") findings = scan_text( Path("sample.py"), - '{"year": REDACTED_YEAR, "month": 4, "day": 17, "hour": 14, "minute": 49}', + "safe text SECRET_BIRTH_TOKEN", + deny_patterns(), ) - - assert {item["rule_id"] for item in findings} >= {"private_birth_dict_tuple"} + assert {item["rule_id"] for item in findings} == {"private_env_pattern_01"} def test_public_release_privacy_scan_has_no_release_findings() -> None: report = build_report() - assert report["status"] == "pass", report["findings"][:20] assert report["finding_count"] == 0 + + +def test_public_release_privacy_scan_supports_unpacked_zip_without_git(tmp_path: Path) -> None: + (tmp_path / "INSTALL.md").write_text("safe install text\n", encoding="utf-8") + (tmp_path / "scratch").mkdir() + (tmp_path / "scratch" / "ignored.md").write_text("SECRET_BIRTH_TOKEN\n", encoding="utf-8") + + assert [path.name for path in iter_release_files(tmp_path)] == ["INSTALL.md"] + report = build_report(tmp_path) + assert report["status"] == "pass", report["findings"] diff --git a/tests/test_real_case_replay_validator.py b/tests/test_real_case_replay_validator.py new file mode 100644 index 00000000..85bb107c --- /dev/null +++ b/tests/test_real_case_replay_validator.py @@ -0,0 +1,73 @@ +from __future__ import annotations + +import json +from pathlib import Path + +from scripts.real_case_replay_validator import validate_manifest +from scripts.unified_consultation_orchestrator import UnifiedConsultationOrchestrator + +ROOT = Path(__file__).resolve().parents[1] + + +def test_real_case_replay_manifest_blocks_when_no_cases_are_imported() -> None: + result = validate_manifest(ROOT / "references/real_case_calibration/replay_manifest.json") + + assert result["status"] == "blocked" + assert result["case_count"] == 0 + assert result["replay_ready_count"] == 0 + assert result["blocked_reason"] == "no_structured_outcome_replay_cases_imported" + + +def test_real_case_replay_validator_accepts_one_structured_case(tmp_path: Path) -> None: + manifest = { + "schema_version": "1.0", + "status": "ready", + "case_schema": "references/real_case_calibration/catalog.schema.json", + "cases": [ + { + "case_id": "public_case_001", + "source": { + "url": "https://example.com/public-case", + "source_grade": "verified_secondary", + "license_or_quote_boundary": "summary_only", + }, + "chart_signature": {"lagna": "Leo", "notable_yogas": ["career_yoga"]}, + "event_outcomes": [ + { + "event_type": "career_breakthrough", + "event_date": "2000-01", + "outcome": "public_success", + } + ], + "similarity": { + "score": 0.72, + "matching_factors": ["D10", "dasha"], + "dissimilar_factors": [], + }, + "replay": { + "outcome_replay_status": "replayed", + "do_not_use_for_prediction": False, + }, + } + ], + } + path = tmp_path / "replay_manifest.json" + path.write_text(json.dumps(manifest), encoding="utf-8") + + result = validate_manifest(path) + + assert result["status"] == "pass" + assert result["case_count"] == 1 + assert result["replay_ready_count"] == 1 + + +def test_orchestrator_exposes_real_case_replay_manifest_status() -> None: + orchestrator = UnifiedConsultationOrchestrator() + route = {"question_type": "career", "primary_theme": "career"} + + packet = orchestrator.real_case_calibration_catalog(route_packet=route, machine_evidence_packet={}) + + replay = packet["outcome_replay_manifest"] + assert replay["status"] == "blocked" + assert replay["case_count"] == 0 + assert packet["required_replay_schema"] == "references/real_case_calibration/catalog.schema.json" diff --git a/tests/test_skill_release_clean_trial.py b/tests/test_skill_release_clean_trial.py new file mode 100644 index 00000000..814fbf23 --- /dev/null +++ b/tests/test_skill_release_clean_trial.py @@ -0,0 +1,70 @@ +from __future__ import annotations + +import json +import subprocess +import sys +import zipfile +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] + + +def test_premium_skill_zip_runs_from_clean_directory(tmp_path: Path) -> None: + zip_path = tmp_path / "jyotish-premium.zip" + release = subprocess.run( + [ + sys.executable, + "scripts/skill_release_package.py", + "--edition", + "premium_cloud_drive", + "--write-zip", + str(zip_path), + ], + cwd=ROOT, + text=True, + capture_output=True, + timeout=120, + check=False, + ) + assert release.returncode == 0, release.stderr or release.stdout + + extract_dir = tmp_path / "clean" + with zipfile.ZipFile(zip_path) as archive: + archive.extractall(extract_dir) + + required = [ + "INSTALL.md", + "USER_PROMPTS.md", + "PACKAGE_ACCEPTANCE.json", + "references/real_case_calibration/replay_manifest.json", + "references/oracle/three_engine_parity_replay_manifest.json", + "references/oracle/western_oracle_adapter_contract.md", + ] + assert [path for path in required if not (extract_dir / path).exists()] == [] + + acceptance = json.loads((extract_dir / "PACKAGE_ACCEPTANCE.json").read_text(encoding="utf-8")) + assert acceptance["status"] == "pass" + + privacy = subprocess.run( + [sys.executable, "scripts/public_release_privacy_scan.py"], + cwd=extract_dir, + text=True, + capture_output=True, + timeout=120, + check=False, + ) + assert privacy.returncode == 0, privacy.stderr or privacy.stdout + + user_acceptance = subprocess.run( + [sys.executable, "scripts/user_invocation_acceptance_check.py"], + cwd=extract_dir, + text=True, + capture_output=True, + timeout=240, + check=False, + ) + assert user_acceptance.returncode == 0, user_acceptance.stderr or user_acceptance.stdout + report = json.loads(user_acceptance.stdout) + assert report["status"] == "pass" + assert report["checks"]["guided_topics_entrypoint"] is True diff --git a/tests/test_skill_release_package.py b/tests/test_skill_release_package.py index ed6582e7..76018767 100644 --- a/tests/test_skill_release_package.py +++ b/tests/test_skill_release_package.py @@ -1,5 +1,6 @@ from __future__ import annotations +import json import zipfile from scripts.skill_release_package import build_package_plan, write_zip @@ -17,6 +18,13 @@ def test_skill_release_package_dry_run_uses_safe_tracked_files() -> None: assert ".env.local" not in plan["files"] assert not any(path.startswith("scratch/") for path in plan["files"]) assert not any("private" in path.lower() for path in plan["files"]) + assert "RELEASE_MANIFEST.json" in plan["generated_files"] + assert "PACKAGE_ACCEPTANCE.json" in plan["generated_files"] + assert plan["required_contracts"]["references/real_case_calibration/replay_manifest.json"] is True + assert plan["required_contracts"]["references/oracle/three_engine_parity_replay_manifest.json"] is True + assert "references/real_case_calibration/replay_manifest.json" in plan["files"] + assert "references/oracle/three_engine_parity_replay_manifest.json" in plan["files"] + assert "references/oracle/western_oracle_adapter_contract.md" in plan["files"] def test_skill_release_package_can_write_zip(tmp_path) -> None: @@ -31,10 +39,20 @@ def test_skill_release_package_can_write_zip(tmp_path) -> None: assert "scripts/skill_release_manifest.py" in names assert "INSTALL.md" in names assert "USER_PROMPTS.md" in names + assert "RELEASE_MANIFEST.json" in names + assert "PACKAGE_ACCEPTANCE.json" in names + assert "references/real_case_calibration/replay_manifest.json" in names + assert "references/oracle/three_engine_parity_replay_manifest.json" in names + assert "references/oracle/western_oracle_adapter_contract.md" in names assert ".env.local" not in names with zipfile.ZipFile(target) as archive: install = archive.read("INSTALL.md").decode("utf-8") prompts = archive.read("USER_PROMPTS.md").decode("utf-8") + acceptance = json.loads(archive.read("PACKAGE_ACCEPTANCE.json").decode("utf-8")) assert "python3 scripts/user_invocation_acceptance_check.py" in install + assert "https://github.com/732642856/yinduzhanxing" in install assert "guided_topics" in prompts assert "official_blocked" in prompts + assert "western_oracle_payload" in prompts + assert acceptance["status"] in {"pass", "blocked"} + assert acceptance["required_contracts"]["references/oracle/three_engine_parity_replay_manifest.json"] is True diff --git a/tests/test_three_engine_parity_replay_validator.py b/tests/test_three_engine_parity_replay_validator.py new file mode 100644 index 00000000..9fed6c0c --- /dev/null +++ b/tests/test_three_engine_parity_replay_validator.py @@ -0,0 +1,48 @@ +from __future__ import annotations + +import json +from pathlib import Path + +from scripts.three_engine_parity_replay_validator import validate_manifest + +ROOT = Path(__file__).resolve().parents[1] + + +def test_three_engine_parity_manifest_blocks_without_oracle_rows() -> None: + result = validate_manifest(ROOT / "references/oracle/three_engine_parity_replay_manifest.json") + + assert result["status"] == "blocked" + assert result["comparison_row_count"] == 0 + assert result["tested"] is False + assert result["blocked_reason"] == "no_same_chart_oracle_rows_imported" + + +def test_three_engine_parity_validator_accepts_one_same_chart_row(tmp_path: Path) -> None: + manifest = { + "case_id": "public_same_chart_001", + "birth_data_policy": "public_case_only", + "status": "tested", + "engines": { + "VedAstro": {"status": "official_verified", "official_raw_response_path": "references/oracle/artifacts/vedastro.json"}, + "PyJHora_JHora": {"status": "tested", "raw_output_path": "references/oracle/artifacts/pyjhora.txt"}, + "jyotishganit": {"status": "tested", "raw_output_path": "references/oracle/artifacts/jyotishganit.json"}, + }, + "comparison_rows": [ + { + "section": "D1", + "field": "Sun.longitude", + "local_value": 27.1, + "oracle_values": {"VedAstro": 27.1, "PyJHora_JHora": 27.1, "jyotishganit": 27.1}, + "status": "match", + } + ], + } + path = tmp_path / "three_engine_parity_replay_manifest.json" + path.write_text(json.dumps(manifest), encoding="utf-8") + + result = validate_manifest(path) + + assert result["status"] == "pass" + assert result["tested"] is True + assert result["comparison_row_count"] == 1 + assert result["match_count"] == 1 diff --git a/tests/test_unified_consultation_orchestrator.py b/tests/test_unified_consultation_orchestrator.py index 707d3423..d73bab02 100644 --- a/tests/test_unified_consultation_orchestrator.py +++ b/tests/test_unified_consultation_orchestrator.py @@ -3,6 +3,7 @@ from __future__ import annotations +from scripts.western_evidence_packet import build_western_evidence_packet from scripts.unified_consultation_orchestrator import UnifiedConsultationOrchestrator @@ -193,6 +194,8 @@ def test_runtime_evidence_log_exposes_blind_packet_case_and_quality_gate_contrac "VedAstro Cloud State", "VedAstro Raw Archive Manifest", "External Engine Cross-Validation", + "Western Cross-Validation", + "Cross-System Arbitration", "Evidence Packet", "Blind Technical Mode", "MEVG / Global Web Evidence", @@ -212,8 +215,54 @@ def test_runtime_evidence_log_exposes_blind_packet_case_and_quality_gate_contrac assert engines["jyotishganit"]["adapter_status"] == "available" assert engines["jyotishganit"]["license"] == "MIT" assert log["external_engine_cross_validation"]["status"] == "partial" + assert log["cross_system_arbitration"]["status"] == "blocked" + assert "western_evidence_packet_missing" in log["cross_system_arbitration"]["blocked_items"] assert log["quality_gate"]["blocked_items"] + +def test_runtime_evidence_log_uses_cross_system_shared_signal_when_packets_align() -> None: + orchestrator = UnifiedConsultationOrchestrator() + western_packet = build_western_evidence_packet( + route_packet={"question_type": "career", "primary_theme": "career"}, + natal={"ascendant": "Virgo", "mc": "Gemini"}, + timing_techniques={"transits": [{"aspect": "Uranus conjunct MC"}]}, + signals=[ + { + "theme": "career_relocation", + "claim": "career_triggered_relocation", + "timing": "2026-08-24..2026-09-28", + "source": "Uranus conjunct MC opposite IC", + } + ], + ) + log = orchestrator.runtime_evidence_log( + surface="api_web", + entry_mode="direct_chart", + route_packet={"question_type": "career", "primary_theme": "career"}, + executed_steps=["compute_chart"], + skipped_steps=[], + machine_evidence_packet={ + "status": "complete", + "sections": {"vedastro_official_raw_archive_manifest": {"status": "used"}}, + "functional_benefic_malefic": {"status": "used"}, + "signals": [ + { + "theme": "career_relocation", + "claim": "career_triggered_relocation", + "timing": "2026-08-24..2026-09-28", + "source": "Saturn/Ketu + Rahu 4H/10H axis", + } + ], + }, + western_evidence_packet=western_packet, + ) + + assert log["cross_system_arbitration"]["status"] == "used" + assert log["cross_system_arbitration"]["shared_signals"][0]["claim"] == "career_triggered_relocation" + audit = {row["technique"]: row for row in log["quality_gate"]["technique_audit_table"]} + assert audit["Western Cross-Validation"]["used"] is True + assert audit["Cross-System Arbitration"]["used"] is True + packet = orchestrator.machine_evidence_packet( chart={"chart": {"planets": {"Sun": {}}, "ascendant": {"sign": "Leo"}}}, route_packet={"question_type": "career", "primary_theme": "career"}, diff --git a/tests/test_user_invocation_acceptance_contract.py b/tests/test_user_invocation_acceptance_contract.py index 590f92dd..56f8fcbc 100644 --- a/tests/test_user_invocation_acceptance_contract.py +++ b/tests/test_user_invocation_acceptance_contract.py @@ -149,4 +149,4 @@ def test_one_command_user_invocation_acceptance_check() -> None: assert report["checks"]["user_invocation_tests"] is True assert report["checks"]["guided_topics_entrypoint"] is True assert report["checks"]["external_adapter_diagnostics"] is True - assert report["external_adapter_status"] in {"pass", "partial"} + assert report["external_adapter_status"] in {"pass", "partial", "complete"} diff --git a/tests/test_western_evidence_packet.py b/tests/test_western_evidence_packet.py new file mode 100644 index 00000000..78f926e8 --- /dev/null +++ b/tests/test_western_evidence_packet.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +"""Western evidence packet contract tests.""" + +from __future__ import annotations + +from scripts.western_evidence_packet import build_western_evidence_packet + + +def test_western_evidence_packet_materializes_auditable_sections() -> None: + packet = build_western_evidence_packet( + route_packet={"question_type": "career", "primary_theme": "career"}, + natal={"ascendant": "Virgo", "mc": "Gemini"}, + timing_techniques={ + "transits": [{"aspect": "Uranus conjunct MC"}], + "solar_return": {"annual_focus": "career"}, + }, + signals=[ + { + "theme": "career_relocation", + "claim": "career_triggered_relocation", + "timing": "2026-08-24..2026-09-28", + "source": "Uranus conjunct MC opposite IC", + } + ], + ) + + assert packet["system"] == "western_astrology" + assert packet["status"] == "complete" + assert packet["sections"]["natal"]["status"] == "used" + assert packet["sections"]["timing_techniques"]["status"] == "used" + assert packet["sections"]["signals"]["status"] == "used" + assert packet["signals"][0]["claim"] == "career_triggered_relocation" + + +def test_western_evidence_packet_marks_missing_timing_as_partial() -> None: + packet = build_western_evidence_packet( + route_packet={"question_type": "career", "primary_theme": "career"}, + natal={"ascendant": "Virgo"}, + timing_techniques=None, + signals=[], + ) + + assert packet["status"] == "partial" + assert "timing_techniques" in packet["missing_sections"] + assert "signals" in packet["missing_sections"] diff --git a/tests/test_western_oracle_adapter.py b/tests/test_western_oracle_adapter.py new file mode 100644 index 00000000..d29fdeb0 --- /dev/null +++ b/tests/test_western_oracle_adapter.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +"""Western external-oracle adapter tests.""" + +from __future__ import annotations + +import json +import subprocess +import sys +from pathlib import Path + +from scripts.western_oracle_adapter import build_packet_from_oracle_payload + +ROOT = Path(__file__).resolve().parents[1] + + +def test_western_oracle_adapter_maps_external_aspects_to_standard_signals() -> None: + packet = build_packet_from_oracle_payload( + { + "source_engine": "kerykeion_external_json", + "natal": {"ascendant": "Virgo", "mc": "Gemini"}, + "timing_techniques": {"solar_return": {"annual_focus": "career"}}, + "aspects": [ + { + "date": "2026-07-07", + "planet": "Uranus", + "aspect": "conjunction", + "target": "MC", + "orb": 0.2, + }, + { + "date": "2026-07-20", + "planet": "Jupiter", + "aspect": "trine", + "target": "Venus", + "orb": 0.4, + }, + { + "date": "2027-07-14", + "planet": "Saturn", + "aspect": "conjunction", + "target": "Sun", + "orb": 0.1, + }, + ], + }, + route_packet={"question_type": "career", "primary_theme": "career"}, + ) + + assert packet["status"] == "complete" + assert packet["source_engine"] == "kerykeion_external_json" + claims = {signal["claim"] for signal in packet["signals"]} + assert "career_triggered_relocation" in claims + assert "client_cooperation_opportunity" in claims + assert "career_responsibility_test" in claims + assert packet["adapter_boundary"]["bundles_external_code"] is False + + +def test_western_oracle_adapter_preserves_explicit_signals() -> None: + packet = build_packet_from_oracle_payload( + { + "source_engine": "manual_astro_com_export", + "natal": {"ascendant": "Virgo"}, + "timing_techniques": {"transits": []}, + "signals": [ + { + "theme": "career", + "claim": "external_project_pivot", + "timing": "2026-07", + "source": "manual review", + } + ], + }, + route_packet={"question_type": "career", "primary_theme": "career"}, + ) + + assert packet["signals"] == [ + { + "theme": "career", + "claim": "external_project_pivot", + "timing": "2026-07", + "source": "manual review", + } + ] + + +def test_western_oracle_adapter_cli_reads_json_file(tmp_path: Path) -> None: + input_path = tmp_path / "western_oracle.json" + input_path.write_text( + json.dumps( + { + "source_engine": "gongshenxing_export", + "natal": {"ascendant": "Virgo", "mc": "Gemini"}, + "aspects": [{"date": "2026-07-07", "planet": "Uranus", "aspect": "conj", "target": "MC"}], + } + ), + encoding="utf-8", + ) + + completed = subprocess.run( + [ + sys.executable, + "scripts/western_oracle_adapter.py", + "--input", + str(input_path), + "--theme", + "career", + "--question-type", + "career", + ], + cwd=ROOT, + text=True, + capture_output=True, + timeout=30, + check=False, + ) + + assert completed.returncode == 0, completed.stderr + packet = json.loads(completed.stdout) + assert packet["source_engine"] == "gongshenxing_export" + assert packet["signals"][0]["claim"] == "career_triggered_relocation"