fix: harden accuracy and api guardrails

This commit is contained in:
732642856
2026-06-26 00:44:09 +08:00
parent ab643c3e4d
commit d11cebacf8
11 changed files with 825 additions and 568 deletions
+2 -1
View File
@@ -82,6 +82,7 @@ Static demo / PWA 发布要求:必须保留 `static_demo_boundary_visible` 说
- quick:快速开发守门,适合普通代码/文案修改后先跑:`python3 scripts/run_quality_gate.py --profile quick`
- browser:完整浏览器守门,覆盖 runtime smoke 与真实浏览器用户路径:`python3 scripts/run_quality_gate.py --profile browser`
- release:发布前守门,包含关键产品文件未跟踪检查、慢速 golden cases、真实案例复验与 Yoga 逻辑报告:`python3 scripts/run_quality_gate.py --profile release`
- accuracy:本地准确率守门,跳过浏览器点击重活,但强制运行真实案例复验、Dasha/Oracle 审计、Yoga 逻辑对照和本地准确率总报告:`python3 scripts/run_quality_gate.py --profile accuracy`
### 真实案例复验与准确率边界
@@ -283,7 +284,7 @@ The AI does NOT require the user to name techniques (e.g., "Chara Dasha"). It au
## Technique Coverage
Current count: **65 techniques** (55 covered, 10 complete, 0 partial, 0 missing)
Current count: **68 techniques** (58 covered, 10 complete, 0 partial, 0 missing)
| Technique | Status | Notes |
|-----------|--------|-------|
+10
View File
@@ -1,5 +1,15 @@
# 印度占星产品化发现记录
## 2026-06-25 Round 25 地毯式碎片扫描前置结论
- 已按用户要求在继续实现前进行整机/多窗口碎片扫描,并生成 `docs/research/whole_machine_fragment_sweep_round25_2026_06_25.md`
- 当前必须作为实现前置读取的规划文件仍是 `task_plan.md``findings.md``progress.md`
- 高价值 Jyotish 资料源不只当前主仓:还包括 `.workbuddy/skills/jyotish-vedic-astrology` 旧 skill 副本、`Documents/星轨talk/engines-repo/jyotish``Documents/Codex/2026-06-20/.../engines-repo/jyotish`、Obsidian Jyotish 研究笔记、Downloads 中的 `印度占星.pdf/印度占星1.pdf/Kimi_Agent_高维印度占星师.zip/jyotish_training.agent.final.docx`、以及当前 repo 内 `references/open_source_sources``benchmarks/jyotish`
- 隐私边界:Downloads/Obsidian/私人 PDF/完整解盘报告仅作为需求和差距发现来源,默认不提交原文、不复制私人出生资料、不上传完整报告。
- 远端状态:HTTPS `git ls-remote` 可达,`origin/codex/release-hygiene-ci` 远端仍停在 `6338cf5`;本地 `bac3748` docs commit 因 SSH 22 超时尚未确认推送成功。后续需使用 SSH-443 或其他可达方式同步。
- Round 25 副手任务已发布,扫描时已看到部分 `antigravity_round25_*` 报告开始生成,但未满 18+ 前不能视为完成。
- Ashtakoot 结论边界:Round 24 “全 0/瞎编”属于需纠正的过强说法;当前 `scripts/ashtakoot.py` 有非零本地规则,但外部 oracle 仍 0/5,不能声称与 JHora/AstroSage/VedAstro 完全一致。
## 开源与本地基线
- `references/open-source-jyotish-scan-2026.md` 已记录可直接复用/对标项目:dashaflow、jyotishganit、panchanga_api、jaimini-tropical、VedicAstro、KPAstroDashboard、vedic_astro_npm、PyJHora、VedAstro、xalen-ephemeris。
+15
View File
@@ -378,3 +378,18 @@
- 完成 Antigravity Round 21 重型副手任务单:新增 `docs/research/antigravity_sidecar_work_order_round21_2026_06_25.md`,把副手工作量扩到 18 个报告包、至少 20 个联网开源/产品复核对象、Top 40 ROI 任务,并明确 Ashtakoot oracle、VedAstro/RaviKarrii/pyhora2 许可证深挖、Trust Center UX、Git 入库、Shadbala 二期校验和 Round 22 执行计划。
- 完成 Ashtakoot 外部 oracle 第一层队列与 validator 强化:新增 `references/oracle/ashtakoot_oracle_cases.json` 5 条 draft cases`oracle_collection_queue.py` 可输出 `target_modules=["ashtakoot"]`README 记录 `Ashtakoot 外部合婚 oracle` 命令;`oracle_evidence_validator.py` 增加 36 分制范围、8 Kuta 分项范围和总分求和一致性拦截,拒绝 99 分、字符串/非法范围与 sum mismatch 的假外部证据包。
- 验证完成:`python3 -m pytest -q tests/test_oracle_evidence_validator.py tests/test_oracle_collection_queue.py` 14 项通过;`python3 -m pytest -q tests/test_ashtakoot.py tests/test_frontend_productization.py::test_dasha_reference_audit_is_documented_and_gated` 53 项通过;`npm run build --prefix jyotish-app` 通过;Ashtakoot queue 输出 5 条 template-only 样本且 `production_tuning_allowed=false`
# 2026-06-25 Round 25 地毯式扫描与副手派工
- 用户强调:所有工作前必须地毯式探索印度占星全部信息,尤其检查不同窗口/多个文件夹中的碎片。
- 已读取 `task_plan.md``findings.md``progress.md`,确认计划中已有“实现前扫描本地碎片、open_source_sources、现有测试与产品差距矩阵”的硬规则。
- 已发布 `docs/research/antigravity_sidecar_work_order_round25_2026_06_25.md`,要求副手纠正 Round 24 Ashtakoot 误判、复核 VedAstro MIT 可复用范围、验收 accuracy profile、继续拆 UI/API/CLI/Prompt Pack/Oracle 缺口。
- 已创建 `docs/research/whole_machine_fragment_sweep_round25_2026_06_25.md`,记录整机扫描范围、高价值本地资料源、Git 远端状态、隐私边界和后续实现前置命令。
- 已发现高价值碎片源:当前主仓、`.workbuddy/skills/jyotish-vedic-astrology``Documents/星轨talk/engines-repo/jyotish``Documents/Codex/2026-06-20/.../engines-repo/jyotish`、Obsidian Jyotish 笔记、Downloads 私人 PDF/zip/docx、`references/open_source_sources``benchmarks/jyotish`
- Git 状态:本地 `bac3748 docs(research): archive antigravity round 23 and 24 audits` 已创建;push 通过 SSH 22 超时失败,远端 HTTPS refs 显示 `codex/release-hygiene-ci` 仍在 `6338cf5`。后续需换 SSH-443 或其他可达方式再推。
- TDD 状态:已为 `run_quality_gate.py --profile accuracy` 写红灯测试,当前失败点是 `accuracy` profile 未实现;实现前已先响应用户要求完成碎片扫描。
- 已发布 `docs/research/antigravity_sidecar_work_order_round26_2026_06_25.md`,要求副手纠正 Round 25 Panchang 过强结论、黑盒验收 accuracy profile、设计 Git 远端替代同步和继续拆解 Ashtakoot/Shadbala/Kuja/Prompt Pack 票据。
- 已实现并验证 `run_quality_gate.py --profile accuracy`:跳过浏览器重活,强制运行真实案例复验、Dasha/Oracle 审计、Yoga 逻辑对照和 `local_accuracy_report.py`;真实运行输出 `Quality gate passed`
- 修复 `scripts/validate_logic_v2.py` 的 set 顺序抖动,让 `references/validation_logic_report.json` 的 false positive/negative 列表稳定排序,避免 accuracy profile 每次制造无意义 diff。
- 完成 Antigravity Round 27 重型副手任务单:新增 `docs/research/antigravity_sidecar_work_order_round27_2026_06_25.md`,把副手工作量提高到 24 份报告,强制复核 Round25/26 归档、accuracy profile 稳定性、Yoga diff 稳定性、SSH-443/HTTPS 同步方案、Shadbala Phase 2、Kuja enum、Prompt Pack 安全护栏、Panchanga 商业级 UI、Ashtakoot oracle/许可证、API/前端隐藏技能 ROI、全机碎片后续读取顺序和 Round28 Top60。
- 完成 Shadbala Evidence Validator Phase 2 第一批实现:在现有 `sthana/dig/kala/chesta/naisargika/drik` schema 上增加 `total_rupa` 必填、单项 Rupa 上限 20.0、七曜六分量求和与 `total_rupa` 0.05 容差校验,避免人工 evidence packet 只填分量但总分不自洽仍被误判有效。TDD 红灯确认 3 个缺口后转绿,聚焦 validator/API 13 项测试通过。
- 完成“有效文件复用索引”:新增 `docs/research/local_reuse_candidate_index_round28_2026_06_26.md`,把当前 repo、旧 WorkBuddy skill、历史 benchmark、本地开源镜像按 `direct current repo reuse``older WorkBuddy skill reuse``local open-source mirrors``benchmark-only sources` 分类,明确未来补 Kuja/Panchanga/Tajika/Chara/Prompt Pack 时必须先查已有文件和许可证边界,再决定是否新写。
File diff suppressed because it is too large Load Diff
+27 -19
View File
@@ -171,6 +171,9 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
self.end_headers()
self.wfile.write(json.dumps(data, ensure_ascii=False, default=str).encode())
def _error_json(self, message, status=500, error_code='ERR_INTERNAL'):
self._json({'success': False, 'error': message, 'error_code': error_code}, status)
def _send_cors_headers(self):
origin = self.headers.get('Origin')
allowed = getattr(self.server, 'allowed_origins', DEFAULT_ALLOWED_ORIGINS)
@@ -182,22 +185,27 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
def do_GET(self):
path = urlparse(self.path).path
if path == '/api/health':
self._json({
'status': 'ok',
'version': '6.9.14',
'modules': 'Chart/KP/Synastry/Prashna/Remedies/Dasha/Varga/Jaimini/Ashtakavarga/Shadbala/Yoga/Aspects/Tajika/Muhurta/BhavaChalit/BhavaBala/Sudarshana/Nakshatra/Transit/RectificationGate/CaseValidation/DivisionalYoga/Kakshya',
})
elif path == '/api/cities':
self._json(list(CITY_DB.keys()))
elif path == '/api/capability_audit':
self._json(self._capability_audit())
elif path == '/api/technique_catalog':
self._json(self._technique_catalog())
elif path == '/api/real_case_revalidation':
self._json(self._real_case_revalidation())
else:
self._json({'error': 'Not found'}, 404)
try:
if path == '/api/health':
self._json({
'status': 'ok',
'version': '6.9.14',
'modules': 'Chart/KP/Synastry/Prashna/Remedies/Dasha/Varga/Jaimini/Ashtakavarga/Shadbala/Yoga/Aspects/Tajika/Muhurta/BhavaChalit/BhavaBala/Sudarshana/Nakshatra/Transit/RectificationGate/CaseValidation/DivisionalYoga/Kakshya',
})
elif path == '/api/cities':
self._json(list(CITY_DB.keys()))
elif path == '/api/capability_audit':
self._json(self._capability_audit())
elif path == '/api/technique_catalog':
self._json(self._technique_catalog())
elif path == '/api/real_case_revalidation':
self._json(self._real_case_revalidation())
else:
self._error_json('Not found', 404, 'ERR_NOT_FOUND')
except Exception:
import logging
logging.exception("[api_server] GET request failed for %s", path)
self._error_json('Internal server error', 500, 'ERR_INTERNAL')
def do_POST(self):
path = urlparse(self.path).path
@@ -306,13 +314,13 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
result = self._compute_technique_example(body)
self._json(result)
else:
self._json({'error': f'Unknown endpoint: {path}'}, 404)
self._error_json(f'Unknown endpoint: {path}', 404, 'ERR_NOT_FOUND')
except BadRequest as e:
self._json({'error': str(e)}, 400)
self._error_json(str(e), 400, 'ERR_BAD_REQUEST')
except Exception:
import logging
logging.exception("[api_server] request failed for %s", path)
self._json({'error': 'Internal server error'}, 500)
self._error_json('Internal server error', 500, 'ERR_INTERNAL')
def _read_json_body(self):
raw_length = self.headers.get('Content-Length', '0')
+27
View File
@@ -26,6 +26,8 @@ LOCAL_ENGINE_MARKERS = [
]
SHADBALA_REQUIRED_PLANETS = ["Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn"]
SHADBALA_REQUIRED_COMPONENTS = ["sthana", "dig", "kala", "chesta", "naisargika", "drik"]
SHADBALA_COMPONENT_MAX_RUPA = 20.0
SHADBALA_TOTAL_TOLERANCE_RUPA = 0.05
ASHTAKOOT_SCORE_RANGES = {
"target.total_score": (0.0, 36.0),
"target.varna": (0.0, 1.0),
@@ -144,6 +146,31 @@ def _validate_shadbala_components(value: Any) -> list[str]:
continue
if component_value < 0:
problems.append(f"invalid_shadbala_component_negative:{planet}.{component}")
continue
if component_value > SHADBALA_COMPONENT_MAX_RUPA:
problems.append(f"invalid_shadbala_component_range:{planet}.{component}")
total_rupa = row.get("total_rupa")
if _is_blank(total_rupa):
problems.append(f"missing_shadbala_total_rupa:{planet}")
continue
if not isinstance(total_rupa, (int, float)) or isinstance(total_rupa, bool):
problems.append(f"invalid_shadbala_total_rupa_type:{planet}")
continue
if total_rupa < 0:
problems.append(f"invalid_shadbala_total_rupa_negative:{planet}")
continue
if total_rupa > SHADBALA_COMPONENT_MAX_RUPA * len(SHADBALA_REQUIRED_COMPONENTS):
problems.append(f"invalid_shadbala_total_rupa_range:{planet}")
continue
numeric_components = [
float(row[component])
for component in SHADBALA_REQUIRED_COMPONENTS
if isinstance(row.get(component), (int, float)) and not isinstance(row.get(component), bool)
]
if len(numeric_components) == len(SHADBALA_REQUIRED_COMPONENTS):
component_sum = sum(numeric_components)
if abs(component_sum - float(total_rupa)) > SHADBALA_TOTAL_TOLERANCE_RUPA:
problems.append(f"shadbala_total_rupa_sum_mismatch:{planet}")
return problems
+29 -2
View File
@@ -97,6 +97,7 @@ QUALITY_GATE_PROFILES = {
"skip_real_cases": True,
"skip_dasha_audit": True,
"skip_oracle_audit": True,
"skip_local_accuracy_report": True,
},
"browser": {
"skip_slow": True,
@@ -108,6 +109,7 @@ QUALITY_GATE_PROFILES = {
"skip_real_cases": True,
"skip_dasha_audit": True,
"skip_oracle_audit": True,
"skip_local_accuracy_report": True,
},
"release": {
"skip_slow": False,
@@ -119,6 +121,19 @@ QUALITY_GATE_PROFILES = {
"skip_real_cases": False,
"skip_dasha_audit": False,
"skip_oracle_audit": False,
"skip_local_accuracy_report": False,
},
"accuracy": {
"skip_slow": True,
"skip_yoga_logic": False,
"skip_frontend_runtime": True,
"skip_frontend_click": True,
"frontend_click_mode": "core",
"check_release_hygiene": False,
"skip_real_cases": False,
"skip_dasha_audit": False,
"skip_oracle_audit": False,
"skip_local_accuracy_report": False,
},
}
@@ -362,7 +377,16 @@ def validate_json_files() -> None:
def run_profile(args: argparse.Namespace) -> dict:
profile = dict(QUALITY_GATE_PROFILES[args.profile])
for key in ["skip_slow", "skip_yoga_logic", "skip_frontend_runtime", "skip_frontend_click", "skip_real_cases", "skip_dasha_audit", "skip_oracle_audit"]:
for key in [
"skip_slow",
"skip_yoga_logic",
"skip_frontend_runtime",
"skip_frontend_click",
"skip_real_cases",
"skip_dasha_audit",
"skip_oracle_audit",
"skip_local_accuracy_report",
]:
if getattr(args, key):
profile[key] = True
if args.frontend_click_mode:
@@ -372,7 +396,7 @@ def run_profile(args: argparse.Namespace) -> dict:
def main() -> int:
parser = argparse.ArgumentParser(description="Run Jyotish skill quality gate")
parser.add_argument("--profile", choices=["quick", "browser", "release"], default="browser", help="Quality gate profile: quick, browser, or release")
parser.add_argument("--profile", choices=["quick", "browser", "release", "accuracy"], default="browser", help="Quality gate profile: quick, browser, release, or accuracy")
parser.add_argument("--skip-slow", action="store_true", help="Skip slow golden-case regressions")
parser.add_argument("--skip-yoga-logic", action="store_true", help="Skip Yoga logic comparison report refresh")
parser.add_argument("--skip-frontend-runtime", action="store_true", help="Skip frontend build and runtime smoke")
@@ -380,6 +404,7 @@ def main() -> int:
parser.add_argument("--skip-real-cases", action="store_true", help="Skip public real-person chart revalidation")
parser.add_argument("--skip-dasha-audit", action="store_true", help="Skip Dasha reference-drift audit")
parser.add_argument("--skip-oracle-audit", action="store_true", help="Skip combined Dasha/Shadbala external oracle boundary audit")
parser.add_argument("--skip-local-accuracy-report", action="store_true", help="Skip consolidated local accuracy report")
parser.add_argument("--frontend-click-mode", choices=["core", "mobile", "offline", "pdf", "workspace", "mobile-trust", "import-files", "all"], default=None, help="Browser click smoke mode for browser/release profiles")
parser.add_argument("--frontend-click-timeout", type=int, default=240, help="Timeout seconds for browser click smoke")
parser.add_argument("--all-tests", action="store_true", help="Run every pytest file, including optional-dependency suites")
@@ -422,6 +447,8 @@ def main() -> int:
run_oracle_collection_queue_and_validator()
if not profile["skip_yoga_logic"]:
run([PYTHON, "scripts/validate_logic_v2.py"], optional=True)
if not profile["skip_local_accuracy_report"]:
run([PYTHON, "scripts/local_accuracy_report.py", "--format", "json"])
print("\nQuality gate passed.")
return 0
+5 -5
View File
@@ -340,15 +340,15 @@ def main():
total_false_negatives += len(false_negatives)
if false_positives:
for rid in false_positives:
for rid in sorted(false_positives):
fp_details.append({
'chart': name,
'rule_id': rid,
'rule_name': rule_id_to_name.get(rid, '?'),
})
if false_negatives:
for rid in false_negatives:
orig_names = [pn for pn, sid in variant_to_rule_id.items() if sid == rid]
for rid in sorted(false_negatives):
orig_names = sorted(pn for pn, sid in variant_to_rule_id.items() if sid == rid)
fn_details.append({
'chart': name,
'rule_id': rid,
@@ -414,8 +414,8 @@ def main():
"recall": round(recall, 4),
"f1": round(f1, 4),
},
"false_positives": fp_details,
"false_negatives": fn_details,
"false_positives": sorted(fp_details, key=lambda row: (row["chart"], row["rule_id"])),
"false_negatives": sorted(fn_details, key=lambda row: (row["chart"], row["rule_id"])),
}
with open(report_path, 'w') as f:
json.dump(report, f, indent=2, ensure_ascii=False)
+49
View File
@@ -4,8 +4,10 @@
from __future__ import annotations
import base64
import json
import os
import sys
from io import BytesIO
import pytest
@@ -27,6 +29,40 @@ def _handler() -> JyotishAPIHandler:
return JyotishAPIHandler.__new__(JyotishAPIHandler)
class _FakeHeaders(dict):
def get(self, key, default=None):
return super().get(key, default)
class _FakeServer:
allowed_origins = DEFAULT_ALLOWED_ORIGINS
class _ResponseCaptureHandler(JyotishAPIHandler):
def __init__(self) -> None:
self.headers = _FakeHeaders()
self.server = _FakeServer()
self.path = '/api/capability_audit'
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 _capability_audit(self):
raise RuntimeError('simulated internal failure')
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
@@ -40,6 +76,19 @@ def test_env_cors_parser_ignores_empty_entries() -> None:
}
def test_get_internal_errors_are_json_wrapped() -> None:
handler = _ResponseCaptureHandler()
handler.do_GET()
assert handler.status_code == 500
assert ('Content-Type', 'application/json; charset=utf-8') in handler.response_headers
payload = handler.payload()
assert payload['success'] is False
assert payload['error'] == 'Internal server error'
assert payload['error_code'] == 'ERR_INTERNAL'
@pytest.mark.parametrize(
('key', 'value', 'minimum', 'maximum'),
[
+19 -1
View File
@@ -891,11 +891,14 @@ def test_quality_gate_declares_fast_browser_release_profiles() -> None:
readme = (ROOT / "README.md").read_text(encoding="utf-8")
for token in [
"--profile",
"choices=[\"quick\", \"browser\", \"release\"]",
"choices=[\"quick\", \"browser\", \"release\", \"accuracy\"]",
"QUALITY_GATE_PROFILES",
"quick",
"browser",
"release",
"accuracy",
"skip_local_accuracy_report",
"scripts/local_accuracy_report.py",
"skip_slow",
"skip_yoga_logic",
"skip_frontend_click",
@@ -913,9 +916,11 @@ def test_quality_gate_declares_fast_browser_release_profiles() -> None:
"quick:快速开发守门",
"browser:完整浏览器守门",
"release:发布前守门",
"accuracy:本地准确率守门",
"python3 scripts/run_quality_gate.py --profile quick",
"python3 scripts/run_quality_gate.py --profile browser",
"python3 scripts/run_quality_gate.py --profile release",
"python3 scripts/run_quality_gate.py --profile accuracy",
]:
assert token in readme
@@ -938,6 +943,19 @@ def test_release_quality_gate_tracks_untracked_product_files() -> None:
assert path in quality_gate.RELEASE_CRITICAL_UNTRACKED_PATHS
def test_accuracy_quality_gate_runs_local_accuracy_report_without_frontend_click() -> None:
quality_gate = load_quality_gate_module()
profile = quality_gate.QUALITY_GATE_PROFILES["accuracy"]
assert profile["skip_frontend_click"] is True
assert profile["skip_frontend_runtime"] is True
assert profile["skip_real_cases"] is False
assert profile["skip_dasha_audit"] is False
assert profile["skip_oracle_audit"] is False
assert profile["skip_yoga_logic"] is False
assert profile["skip_local_accuracy_report"] is False
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 [
+107 -5
View File
@@ -15,13 +15,15 @@ SHADBALA_COMPONENTS = ["sthana", "dig", "kala", "chesta", "naisargika", "drik"]
def complete_shadbala_components() -> dict[str, dict[str, float]]:
return {
planet: {
component: float(40 + planet_index * 3 + component_index)
result = {}
for planet_index, planet in enumerate(SHADBALA_PLANETS):
row = {
component: round(0.1 + planet_index * 0.01 + component_index * 0.02, 4)
for component_index, component in enumerate(SHADBALA_COMPONENTS)
}
for planet_index, planet in enumerate(SHADBALA_PLANETS)
}
row["total_rupa"] = round(sum(row[component] for component in SHADBALA_COMPONENTS), 4)
result[planet] = row
return result
def build_queue() -> dict:
@@ -209,6 +211,106 @@ def test_validator_rejects_non_numeric_or_negative_shadbala_components(tmp_path:
assert "invalid_shadbala_component_negative:Moon.dig" in first["problems"]
def test_validator_rejects_missing_shadbala_total_rupa(tmp_path: Path) -> None:
queue = build_queue()
packet = queue["tasks"][0]["evidence_packet"]
shadbala = complete_shadbala_components()
del shadbala["Sun"]["total_rupa"]
packet["metadata"] = {
"tool_name": "JHora",
"tool_version_or_url": "manual-screenshot-v1",
"capture_date": "2026-06-25",
"source_artifact": "references/oracle/artifacts/manual_jhora_user_REDACTED_YEAR.png",
"ayanamsa": "lahiri",
"node_mode": "mean",
"timezone": "UTC+08:00",
"operator_note": "Manual external screenshot; values typed from JHora screen.",
}
packet["target_placeholders"] = {
"target.moon_sidereal_longitude_deg": 311.7897,
"target.vimshottari_start_date": "1986-05-18",
"target.shadbala_components": shadbala,
}
packet["status"] = "external_verified"
queue_path = tmp_path / "queue.json"
queue_path.write_text(json.dumps(queue, ensure_ascii=False), encoding="utf-8")
completed = run_validator(queue_path)
assert completed.returncode == 0, completed.stderr or completed.stdout
report = json.loads(completed.stdout)
first = report["packets"][0]
assert first["valid"] is False
assert "missing_shadbala_total_rupa:Sun" in first["problems"]
def test_validator_rejects_shadbala_component_values_above_20_rupas(tmp_path: Path) -> None:
queue = build_queue()
packet = queue["tasks"][0]["evidence_packet"]
shadbala = complete_shadbala_components()
shadbala["Sun"]["sthana"] = 20.01
shadbala["Sun"]["total_rupa"] = round(sum(shadbala["Sun"][component] for component in SHADBALA_COMPONENTS), 4)
packet["metadata"] = {
"tool_name": "JHora",
"tool_version_or_url": "manual-screenshot-v1",
"capture_date": "2026-06-25",
"source_artifact": "references/oracle/artifacts/manual_jhora_user_REDACTED_YEAR.png",
"ayanamsa": "lahiri",
"node_mode": "mean",
"timezone": "UTC+08:00",
"operator_note": "Manual external screenshot; values typed from JHora screen.",
}
packet["target_placeholders"] = {
"target.moon_sidereal_longitude_deg": 311.7897,
"target.vimshottari_start_date": "1986-05-18",
"target.shadbala_components": shadbala,
}
packet["status"] = "external_verified"
queue_path = tmp_path / "queue.json"
queue_path.write_text(json.dumps(queue, ensure_ascii=False), encoding="utf-8")
completed = run_validator(queue_path)
assert completed.returncode == 0, completed.stderr or completed.stdout
report = json.loads(completed.stdout)
first = report["packets"][0]
assert first["valid"] is False
assert "invalid_shadbala_component_range:Sun.sthana" in first["problems"]
def test_validator_rejects_shadbala_total_rupa_sum_mismatch(tmp_path: Path) -> None:
queue = build_queue()
packet = queue["tasks"][0]["evidence_packet"]
shadbala = complete_shadbala_components()
shadbala["Sun"]["total_rupa"] = round(shadbala["Sun"]["total_rupa"] + 0.06, 4)
packet["metadata"] = {
"tool_name": "JHora",
"tool_version_or_url": "manual-screenshot-v1",
"capture_date": "2026-06-25",
"source_artifact": "references/oracle/artifacts/manual_jhora_user_REDACTED_YEAR.png",
"ayanamsa": "lahiri",
"node_mode": "mean",
"timezone": "UTC+08:00",
"operator_note": "Manual external screenshot; values typed from JHora screen.",
}
packet["target_placeholders"] = {
"target.moon_sidereal_longitude_deg": 311.7897,
"target.vimshottari_start_date": "1986-05-18",
"target.shadbala_components": shadbala,
}
packet["status"] = "external_verified"
queue_path = tmp_path / "queue.json"
queue_path.write_text(json.dumps(queue, ensure_ascii=False), encoding="utf-8")
completed = run_validator(queue_path)
assert completed.returncode == 0, completed.stderr or completed.stdout
report = json.loads(completed.stdout)
first = report["packets"][0]
assert first["valid"] is False
assert "shadbala_total_rupa_sum_mismatch:Sun" in first["problems"]
def test_validator_rejects_invalid_ashtakoot_score_ranges(tmp_path: Path) -> None:
queue = build_queue_from_file(ROOT / "references/oracle/ashtakoot_oracle_cases.json")
packet = queue["tasks"][0]["evidence_packet"]