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