fix: harden accuracy and api guardrails
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'),
|
||||
[
|
||||
|
||||
@@ -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 [
|
||||
|
||||
@@ -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"]
|
||||
|
||||
Reference in New Issue
Block a user