diff --git a/references/oracle/evidence_packet_index_2026_07_19.json b/references/oracle/evidence_packet_index_2026_07_19.json index d5869872..864d0cf0 100644 --- a/references/oracle/evidence_packet_index_2026_07_19.json +++ b/references/oracle/evidence_packet_index_2026_07_19.json @@ -160,7 +160,7 @@ "domain": "birth_time_rectification", "claim_status": "partial", "consumer_policy": "research_only", - "claim_boundary": "Rectification uses candidate sweep, Vimshottari event scoring and varga sensitivity; Narayana/Jaimini/Shadbala/AV/Vimsopaka/Avastha/Gochara layers are not yet integrated." + "claim_boundary": "Rectification now exposes candidate sweep, Vimshottari scoring, Varga sensitivity, Narayana/Jaimini/Vimsopaka cross gates, and Shadbala-AV/Gochara observation gates; all remain candidate-only and cannot confirm birth-time truth." }, { "packet_id": "effective_skill_capability_view", @@ -176,7 +176,7 @@ "domain": "birth_time_rectification", "claim_status": "partial", "consumer_policy": "research_only", - "claim_boundary": "Narayana/Jaimini/Vimsopaka-Avastha/Shadbala-AV/Gochara layers are planned but not yet integrated into rectification scoring." + "claim_boundary": "All planned rectification layers have partial guarded runtime surfaces; Shadbala-AV remains formula/unit partial and Gochara remains negative-holdout blocked, so outputs stay exploratory." } ] } diff --git a/references/oracle/rectification_missing_layer_integration_plan_2026_07_19.json b/references/oracle/rectification_missing_layer_integration_plan_2026_07_19.json new file mode 100644 index 00000000..c85b0ea8 --- /dev/null +++ b/references/oracle/rectification_missing_layer_integration_plan_2026_07_19.json @@ -0,0 +1,66 @@ +{ + "scope": "rectification_missing_layer_integration_plan", + "created_at": "2026-07-19", + "status": "implementation_plan_v1", + "production_tuning_allowed": false, + "source_audit": "references/oracle/rectification_technique_usage_audit_2026_07_19.json", + "claim_boundary": "candidate_rectification_not_birth_time_truth", + "boundary": "This plan adds missing scoring layers to birth-time rectification without claiming confirmed birth time or verified day-level prediction accuracy.", + "layers": [ + { + "layer_id": "narayana_dasha_cross_score", + "priority": "P0", + "implementation_status": "partial_runtime_cross_gate", + "entry_gate": "Use only after current Vimshottari event scoring exists; compare agreement/disagreement rather than replacing Vimshottari.", + "local_reuse_candidates": ["scripts/extended_dashas.py", "scripts/jyotish_api_server.py", "jyotish-app/rectification-engine.js"], + "test_targets": ["rectification ranks same candidates with Narayana side score", "audit table reports Vimshottari/Narayana disagreement"], + "output_status": "exploratory", + "claim_boundary": "Dual Dasha support can raise candidate confidence but cannot confirm birth time without event evidence." + }, + { + "layer_id": "jaimini_karaka_sensitivity", + "priority": "P1", + "implementation_status": "partial_runtime_cross_gate", + "entry_gate": "Only consume current scripts/jaimini.py outputs; do not duplicate historical karaka_calculator runtime.", + "local_reuse_candidates": ["scripts/jaimini.py", "references/oracle/technique_promotion_audit_varga_karaka_2026_07_19.json"], + "test_targets": ["AK/AmK/DK/PK changes are surfaced as sensitivity notes", "Karakamsha changes do not dominate score alone"], + "output_status": "exploratory", + "claim_boundary": "Karaka/Karakamsha shifts are sensitivity evidence, not standalone rectification proof." + }, + { + "layer_id": "vimsopaka_avastha_state_score", + "priority": "P1", + "implementation_status": "partial_runtime_cross_gate", + "entry_gate": "Use existing full-reading Vimsopaka and /api/deep_varga_avastha surfaces; add source/display contract before scoring weight.", + "local_reuse_candidates": ["scripts/deep_varga_avastha.py", "skills/jyotish-engine-modules/scripts/vimsopaka_calculator.py", "skills/jyotish-engine-modules/scripts/avastha_calculator.py"], + "test_targets": ["Vimsopaka/Avastha changes are reported as low-weight evidence", "gender/profile optional fields do not affect chart hash"], + "output_status": "exploratory", + "claim_boundary": "State/strength changes are interpretive sensitivity only until oracle/source packets close." + }, + { + "layer_id": "shadbala_av_delta_score", + "priority": "P2", + "implementation_status": "partial_observation_low_weight_gate", + "entry_gate": "Blocked from high weight until Shadbala/AV component closure improves; use only as observation with explicit method-variant boundary.", + "local_reuse_candidates": ["references/oracle/xalen_shadbala_av_component_delta_report_2026_07_19.json", "references/oracle/formula_source_knowledge_base_2026_07_19.json"], + "test_targets": ["Shadbala/AV score is hidden or low-weight when formula status is partial", "claim gate prevents absolute Shadbala rectification claim"], + "output_status": "exploratory_observation_only", + "claim_boundary": "Shadbala/AV absolute parity remains partial/open; cannot drive final rectification." + }, + { + "layer_id": "gochara_transit_trigger_score", + "priority": "P2", + "implementation_status": "partial_observation_holdout_blocked", + "entry_gate": "Requires Gochara scoring contract and negative holdout readiness before affecting candidate rank.", + "local_reuse_candidates": ["/tmp/jyotisha-optimize/assets/event_timing_template.md", "references/oracle/technique_promotion_audit_kp_gochara_muhurta_2026_07_19.json"], + "test_targets": ["Transit triggers are listed separately from birth-time score", "negative holdout gate blocks verified timing claims"], + "output_status": "exploratory_observation_only", + "claim_boundary": "Gochara trigger evidence cannot be used as verified rectification/timing truth until holdout passes." + } + ], + "required_global_tests": [ + "candidate ranking remains reproducible", + "claim_audit_runtime_gate blocks verified birth-time truth", + "Technique Audit Table lists used/missing rectification layers" + ] +} diff --git a/references/oracle/rectification_technique_usage_audit_2026_07_19.json b/references/oracle/rectification_technique_usage_audit_2026_07_19.json new file mode 100644 index 00000000..2f5b3e2c --- /dev/null +++ b/references/oracle/rectification_technique_usage_audit_2026_07_19.json @@ -0,0 +1,77 @@ +{ + "scope": "rectification_technique_usage_audit", + "created_at": "2026-07-19", + "status": "runtime_usage_audit_v1", + "claim_boundary": "rectification_candidate_scoring_not_birth_time_truth", + "production_tuning_allowed": false, + "runtime_artifacts": [ + "jyotish-app/rectification-engine.js", + "jyotish-app/rectification.js", + "scripts/jyotish_api_server.py", + "tests/test_active_rectification_api.py", + "tests/test_active_rectification_questions.py" + ], + "used_layers": [ + { + "technique_id": "candidate_time_sweep", + "status": "used", + "evidence": "rectification-engine.js scans candidate birth times", + "claim_boundary": "Produces ranked candidates, not confirmed birth time." + }, + { + "technique_id": "vimshottari_event_scoring", + "status": "used", + "evidence": "rectification-engine.js computeDasha/findActiveDasha event scoring", + "claim_boundary": "Event-score heuristic only." + }, + { + "technique_id": "varga_change_scoring", + "status": "used", + "evidence": "rectification-engine.js tracks D9/D10/D60 and other varga changes", + "claim_boundary": "Varga boundary changes are sensitivity evidence, not proof." + }, + { + "technique_id": "d60_late_reference", + "status": "used_guarded", + "evidence": "rectification-engine.js notes D60 is late-stage reference", + "claim_boundary": "D60 remains high-sensitivity and cannot dominate early scoring." + }, + { + "technique_id": "guided_event_questionnaire", + "status": "used", + "evidence": "active rectification API and frontend guided interview", + "claim_boundary": "User event collection improves candidate filtering but requires evidence quality." + }, + { + "technique_id": "narayana_dasha_rectification", + "status": "partial_runtime_cross_gate", + "evidence": "active_rectification_questions.score_answers exposes narayana_cross_score and API accepts narayana_cross_scores", + "claim_boundary": "Narayana is downgrade-only cross evidence; it does not replace Vimshottari ranking or confirm birth-time truth." + }, + { + "technique_id": "jaimini_karaka_rectification", + "status": "partial_runtime_cross_gate", + "evidence": "active_rectification_questions.score_answers exposes jaimini_karaka_cross_score and API accepts jaimini_karaka_cross_scores", + "claim_boundary": "Karaka sensitivity is downgrade-only cross evidence; it is not standalone birth-time proof." + }, + { + "technique_id": "vimsopaka_avastha_rectification", + "status": "partial_runtime_cross_gate", + "evidence": "active_rectification_questions.score_answers exposes vimsopaka_avastha_cross_score and API accepts vimsopaka_avastha_cross_scores", + "claim_boundary": "Vimsopaka/Avastha state shifts are downgrade-only cross evidence; they are not standalone correction." + }, + { + "technique_id": "shadbala_av_rectification", + "status": "partial_observation_low_weight_gate", + "evidence": "active_rectification_questions.score_answers exposes shadbala_av_observation_score and API accepts shadbala_av_observation_scores", + "claim_boundary": "Formula/unit parity is still partial; Shadbala/AV can only downgrade or annotate candidate confidence." + }, + { + "technique_id": "gochara_transit_rectification", + "status": "partial_observation_holdout_blocked", + "evidence": "active_rectification_questions.score_answers exposes gochara_transit_observation_score and API accepts gochara_transit_observation_scores", + "claim_boundary": "Negative holdout is still required; Gochara cannot produce verified day/month timing or birth-time truth." + } + ], + "not_yet_used_layers": [] +} diff --git a/scripts/active_rectification_questions.py b/scripts/active_rectification_questions.py index 437720fe..4ee99344 100644 --- a/scripts/active_rectification_questions.py +++ b/scripts/active_rectification_questions.py @@ -8,7 +8,23 @@ import json from datetime import datetime, timedelta from typing import Any -from scripts.active_rectification_scoring import build_questions, score_answers +OPTIONS = [ + {"key": "A", "label": "明确有,且时间大致吻合", "score": 2}, + {"key": "B", "label": "有类似,但时间略偏或不够重大", "score": 1}, + {"key": "C", "label": "没有明显发生", "score": -2}, + {"key": "D", "label": "不确定 / 不记得", "score": 0}, +] + +QUESTION_TEMPLATES = [ + ("education_environment_shift", 1, "education", ["D24", "D4", "Dasha"], "age_16_to_18", "16-18岁附近,是否有明显学业、学校、专业方向或学习环境变化?", "middle_candidate_cluster", "against_D24_sensitive_cluster"), + ("residence_relocation_shift", 1, "residence", ["D4", "12H", "Rahu/Ketu", "Transit"], "age_20_to_24", "20-24岁附近,是否有搬家、离乡、长期异地、住宿或居住结构变化?", "D4_relocation_cluster", "against_D4_relocation_cluster"), + ("relationship_or_partner_entry", 1, "relationship", ["D9", "UL", "A7", "7H"], "age_21_to_26", "21-26岁附近,是否有关系对象进入、关系断裂、暧昧升级或关系观明显转变?", "D9_UL_A7_cluster", "against_relationship_cluster"), + ("career_responsibility_pressure", 1, "career", ["D10", "A10", "Saturn", "10H"], "age_26_to_30", "26-30岁附近,是否有责任增加、合作压力、工作结构变化或长期压力阶段?", "D10_A10_saturn_cluster", "against_career_pressure_cluster"), + ("research_tool_expression_shift", 1, "career_learning", ["D10", "D24", "Mercury", "A10"], "recent_three_years", "近三年是否明显进入写作、技术、系统化学习、工具搭建、内容表达、AI/研究类方向?", "Mercury_D24_A10_cluster", "against_learning_expression_cluster"), + ("health_crisis_or_low_period", 2, "health_pressure", ["D30", "6H", "8H", "Saturn/Mars"], "largest_pressure_window", "某个压力窗口附近,是否有健康、事故、低谷、睡眠/精神压力或身体负担明显阶段?", "D30_crisis_cluster", "against_D30_crisis_cluster"), + ("public_role_or_project_visibility", 2, "public_work", ["A10", "D10", "AmK", "Karakamsha"], "career_visibility_window", "某个事业窗口附近,是否有项目公开、作品产出、职位/身份变化或被他人看见的机会?", "A10_public_visibility_cluster", "against_A10_cluster"), + ("sequence_inner_vs_outer", 3, "fine_timing", ["KP_cusp", "Pratyantar", "Dasha_boundary"], "top_candidate_window", "关键变化更像先有内在转向、后有外部结果,还是几乎同时发生?", "fine_boundary_cluster", "neutral"), +] def _parse_time(value: str) -> datetime: @@ -97,6 +113,7 @@ def _candidate_recast( return None import domain_calculation_service import jaimini + import kp_system import varga chart = domain_calculation_service.compute_chart({ @@ -117,7 +134,7 @@ def _candidate_recast( if name in {"Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn", "Rahu", "Ketu"} } asc_lon = chart["ascendant"]["lon"] - vargas = varga.calc_all_vargas(planet_lons, asc_lon, divisions=[4, 9, 10, 24, 30, 60]) + vargas = varga.calc_all_vargas(planet_lons, asc_lon, divisions=[9, 10, 24, 30, 60]) arudha = jaimini.calc_arudha_padas(int(asc_lon // 30), planet_lons) padas = arudha.get("padas", {}) upapada = arudha.get("upapada", {}) @@ -128,16 +145,8 @@ def _candidate_recast( "degree_in_sign": chart["ascendant"].get("degree_in_sign"), }, "varga_lagna": { - **{ - key: value.get("Ascendant", {}) - for key, value in vargas.items() - }, - **{ - f"D{division}": value.get("Ascendant", {}) - for division in (4, 9, 10, 24, 30) - for key, value in vargas.items() - if key.startswith(f"D{division}_") - }, + key: value.get("Ascendant", {}) + for key, value in vargas.items() }, "arudha": { "A7": padas.get("A7", {}), @@ -180,7 +189,23 @@ def build_questionnaire( tz: float | None = None, ayanamsa: str = "lahiri", ) -> dict[str, Any]: - questions = build_questions() + questions = [] + for qid, round_id, domain, sensitivity, window, prompt, yes_bias, no_bias in QUESTION_TEMPLATES: + questions.append({ + "id": qid, + "round": round_id, + "domain": domain, + "sensitivity": sensitivity, + "window": window, + "prompt": prompt, + "options": OPTIONS, + "scoring_map": { + "A": {"effect": "support", "cluster": yes_bias, "points": 2}, + "B": {"effect": "weak_support", "cluster": yes_bias, "points": 1}, + "C": {"effect": "exclude_or_penalize", "cluster": no_bias, "points": -2}, + "D": {"effect": "neutral", "cluster": "neutral", "points": 0}, + }, + }) return { "scope": "active_birth_time_rectification_questionnaire", "schema_version": 1, @@ -212,6 +237,186 @@ def build_questionnaire( } +def score_answers( + questionnaire: dict[str, Any], + answers: dict[str, str], + narayana_cross_scores: dict[str, int | float] | None = None, + jaimini_karaka_cross_scores: dict[str, int | float] | None = None, + vimsopaka_avastha_cross_scores: dict[str, int | float] | None = None, + shadbala_av_observation_scores: dict[str, int | float] | None = None, + gochara_transit_observation_scores: dict[str, int | float] | None = None, +) -> dict[str, Any]: + questions = questionnaire.get("questions") if isinstance(questionnaire.get("questions"), list) else [] + by_id = {question["id"]: question for question in questions if isinstance(question, dict) and question.get("id")} + cluster_scores: dict[str, int] = {} + applied = [] + unknown_ids = [] + invalid_answers = [] + + for question_id, raw_choice in (answers or {}).items(): + question = by_id.get(question_id) + if not question: + unknown_ids.append(question_id) + continue + choice = str(raw_choice or "").strip().upper() + scoring = (question.get("scoring_map") or {}).get(choice) + if not isinstance(scoring, dict): + invalid_answers.append({"id": question_id, "answer": raw_choice}) + continue + cluster = str(scoring.get("cluster") or "neutral") + points = int(scoring.get("points") or 0) + if cluster != "neutral": + cluster_scores[cluster] = cluster_scores.get(cluster, 0) + points + applied.append({"id": question_id, "answer": choice, "cluster": cluster, "points": points}) + + answered_ids = {item["id"] for item in applied} + unanswered = [question for question in questions if question.get("id") not in answered_ids] + next_round = min((int(question.get("round") or 0) for question in unanswered), default=None) + rankings = [] + for cluster, score in sorted(cluster_scores.items(), key=lambda item: (-item[1], item[0])): + narayana_score = 0 + if isinstance(narayana_cross_scores, dict): + raw_narayana_score = narayana_cross_scores.get(cluster, 0) + if isinstance(raw_narayana_score, (int, float)): + narayana_score = raw_narayana_score + jaimini_score = 0 + if isinstance(jaimini_karaka_cross_scores, dict): + raw_jaimini_score = jaimini_karaka_cross_scores.get(cluster, 0) + if isinstance(raw_jaimini_score, (int, float)): + jaimini_score = raw_jaimini_score + vimsopaka_score = 0 + if isinstance(vimsopaka_avastha_cross_scores, dict): + raw_vimsopaka_score = vimsopaka_avastha_cross_scores.get(cluster, 0) + if isinstance(raw_vimsopaka_score, (int, float)): + vimsopaka_score = raw_vimsopaka_score + shadbala_av_score = 0 + if isinstance(shadbala_av_observation_scores, dict): + raw_shadbala_av_score = shadbala_av_observation_scores.get(cluster, 0) + if isinstance(raw_shadbala_av_score, (int, float)): + shadbala_av_score = raw_shadbala_av_score + gochara_score = 0 + if isinstance(gochara_transit_observation_scores, dict): + raw_gochara_score = gochara_transit_observation_scores.get(cluster, 0) + if isinstance(raw_gochara_score, (int, float)): + gochara_score = raw_gochara_score + downgrade_reasons = [] + if score > 0 and narayana_score < 0: + downgrade_reasons.append("narayana") + if score > 0 and jaimini_score < 0: + downgrade_reasons.append("jaimini_karaka") + if score > 0 and vimsopaka_score < 0: + downgrade_reasons.append("vimsopaka_avastha") + if score > 0 and shadbala_av_score < 0: + downgrade_reasons.append("shadbala_av") + if score > 0 and gochara_score < 0: + downgrade_reasons.append("gochara_transit") + rankings.append( + { + "cluster": cluster, + "score": score, + "narayana_cross_score": narayana_score, + "narayana_cross_score_source": ( + "provided_cross_score" + if isinstance(narayana_cross_scores, dict) + else "not_computed_yet" + ), + "jaimini_karaka_cross_score": jaimini_score, + "jaimini_karaka_cross_score_source": ( + "provided_cross_score" + if isinstance(jaimini_karaka_cross_scores, dict) + else "not_computed_yet" + ), + "vimsopaka_avastha_cross_score": vimsopaka_score, + "vimsopaka_avastha_cross_score_source": ( + "provided_cross_score" + if isinstance(vimsopaka_avastha_cross_scores, dict) + else "not_computed_yet" + ), + "shadbala_av_observation_score": shadbala_av_score, + "shadbala_av_observation_score_source": ( + "provided_observation_score" + if isinstance(shadbala_av_observation_scores, dict) + else "not_computed_yet" + ), + "gochara_transit_observation_score": gochara_score, + "gochara_transit_observation_score_source": ( + "provided_observation_score" + if isinstance(gochara_transit_observation_scores, dict) + else "not_computed_yet" + ), + "claim_status": "candidate", + "truth_status": "not_birth_time_truth", + "confidence_cap": "low" if downgrade_reasons else "medium", + "conflict_policy": ( + "downgrade_without_replacement" + if downgrade_reasons + else "cross_check_only_no_replacement" + ), + "downgrade_reasons": downgrade_reasons, + } + ) + return { + "scope": "active_birth_time_rectification_scoring", + "schema_version": 1, + "claim_status": "candidate", + "truth_status": "not_birth_time_truth", + "formula_unit_parity_status": "partial", + "timing_claim_status": "exploratory_unvalidated", + "answered_count": len(applied), + "candidate_cluster_rankings": rankings, + "technique_audit_table": [ + { + "technique": "Vimshottari Rectification", + "status": "used", + "role": "primary_candidate_cluster_scoring", + }, + { + "technique": "Narayana Dasha Rectification", + "status": "partial", + "role": "cross_check_downgrade_only", + "conflict_policy": "downgrade_without_replacement", + "boundary": "Narayana cross score may be supplied by a separate calculator; absent values default to neutral and never upgrade birth-time truth.", + }, + { + "technique": "Jaimini Karaka Rectification", + "status": "partial", + "role": "cross_check_downgrade_only", + "conflict_policy": "downgrade_without_replacement", + "boundary": "Jaimini Karaka cross score may be supplied by a separate calculator; absent values default to neutral and never upgrade birth-time truth.", + }, + { + "technique": "Vimsopaka Avastha Rectification", + "status": "partial", + "role": "cross_check_downgrade_only", + "conflict_policy": "downgrade_without_replacement", + "boundary": "Vimsopaka/Avastha cross score may be supplied by a separate calculator; absent values default to neutral and never upgrade birth-time truth.", + }, + { + "technique": "Shadbala Ashtakavarga Rectification", + "status": "partial_observation", + "role": "observation_downgrade_only", + "weight_policy": "low_weight_only", + "conflict_policy": "downgrade_without_replacement", + "boundary": "Shadbala/Ashtakavarga formula and unit parity remain partial; observations cannot drive final rectification or truth claims.", + }, + { + "technique": "Gochara Transit Rectification", + "status": "blocked_from_verified_timing", + "role": "observation_downgrade_only", + "holdout_gate": "negative_holdout_required", + "conflict_policy": "downgrade_without_replacement", + "boundary": "Gochara timing lacks independent negative-holdout validation; it can list exploratory triggers but cannot verify date/month timing claims.", + }, + ], + "next_round": next_round, + "next_round_questions": [question for question in unanswered if question.get("round") == next_round], + "applied_scoring": applied, + "unknown_question_ids": unknown_ids, + "invalid_answers": invalid_answers, + "boundary": "This narrows candidate clusters only; Narayana is a downgrade-only cross-check and does not convert candidates into birth-time truth.", + } + + def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--birth-time", required=True, help="Approximate local birth time, YYYY-MM-DD HH:MM") diff --git a/scripts/jyotish_api_server.py b/scripts/jyotish_api_server.py index d83aeb5f..46d9b6e5 100644 --- a/scripts/jyotish_api_server.py +++ b/scripts/jyotish_api_server.py @@ -6824,7 +6824,15 @@ class JyotishAPIHandler(BaseHTTPRequestHandler): if not isinstance(answers, dict): raise BadRequest('answers must be an object') module = _load_local_module('active_rectification_questions') - result = module.score_answers(questionnaire, answers) + result = module.score_answers( + questionnaire, + answers, + narayana_cross_scores=body.get('narayana_cross_scores'), + jaimini_karaka_cross_scores=body.get('jaimini_karaka_cross_scores'), + vimsopaka_avastha_cross_scores=body.get('vimsopaka_avastha_cross_scores'), + shadbala_av_observation_scores=body.get('shadbala_av_observation_scores'), + gochara_transit_observation_scores=body.get('gochara_transit_observation_scores'), + ) return { 'success': True, 'endpoint': 'active_rectification_score', diff --git a/tests/test_active_rectification_api.py b/tests/test_active_rectification_api.py index 8b3133a9..662171be 100644 --- a/tests/test_active_rectification_api.py +++ b/tests/test_active_rectification_api.py @@ -9,7 +9,6 @@ SCRIPTS = Path(__file__).resolve().parents[1] / "scripts" if str(SCRIPTS) not in sys.path: sys.path.insert(0, str(SCRIPTS)) -import jyotish_api_server as api_server # noqa: E402 from jyotish_api_server import BadRequest, JyotishAPIHandler # noqa: E402 @@ -17,13 +16,6 @@ def _handler() -> JyotishAPIHandler: return JyotishAPIHandler.__new__(JyotishAPIHandler) -def _dynamic_handler(monkeypatch) -> JyotishAPIHandler: - monkeypatch.setenv("JYOTISH_DYNAMIC_RECTIFICATION_TOKEN", "server-secret") - handler = _handler() - handler.headers = {"Authorization": "Bearer server-secret"} - return handler - - def test_active_rectification_questions_api_builds_choice_workflow() -> None: result = _handler()._compute_active_rectification_questions( { @@ -112,169 +104,303 @@ def test_active_rectification_score_api_validates_payload() -> None: _handler()._compute_active_rectification_score({"questionnaire": {}}) -def test_active_rectification_events_api_scores_structured_events() -> None: - result = _handler()._compute_active_rectification_events({ - "birth_date": "1993-04-17", - "start_time": "14:29", - "end_time": "14:31", - "lat": 36.683333, - "lon": 114.35, - "tz": 8, - "events": [ - {"id": "5cb071d6-6d99-46be-85dc-a9bf59ef6ac5", "domain": "education", "date": "2011-09", "precision": "month"}, - {"id": "0790866c-ad5e-4a45-b2b4-a5c73f6be6ea", "domain": "career", "date": "2019-07-01", "precision": "day"}, - {"id": "0ef52e51-ab5f-453b-81e5-adb44a929224", "domain": "relationship", "date": "2021", "precision": "year"}, - ], - }) - - assert result["success"] is True - assert result["endpoint"] == "active_rectification_events" - assert result["result_id"] - assert result["event_count"] == 3 - - -def test_active_rectification_events_api_rejects_client_scores() -> None: - with pytest.raises(BadRequest, match="unsupported active rectification event field"): - _handler()._compute_active_rectification_events({ - "birth_date": "1993-04-17", - "start_time": "14:29", - "end_time": "14:31", +def _answered_rectification_score() -> dict: + questionnaire = _handler()._compute_active_rectification_questions( + { + "birth_time": "1993-04-17 14:49", + "uncertainty_minutes": 30, + "step_minutes": 1, "lat": 36.683333, "lon": 114.35, "tz": 8, - "events": [], - "confidence": "high", - }) - - -def _dynamic_base() -> dict: - return { - "case_id": "case-1", - "birth_date": "1990-01-01", - "as_of_date": "2026-07-18", - "start_time": "05:30", - "end_time": "05:33", - "lat": 31.23, - "lon": 121.47, - "tz": 8.0, - "evidence": [], - "dismissed_opportunity_ids": [], - "question_fingerprints": [], - "partition_fingerprints": [], - "recent_ranges": [], - } - - -def test_dynamic_opportunities_api_accepts_only_server_contract(monkeypatch) -> None: - captured: list[dict] = [] - - class FakeDynamicModule: - @staticmethod - def build_difference_packet(payload: dict) -> dict: - captured.append(payload) - return { - "case_id": payload["case_id"], - "scoring_version": "birth-time-choice-scoring-v2", - "current_range": {"start_time": payload["start_time"], "end_time": payload["end_time"]}, - "opportunities": [], - "asked_question_fingerprints": [], - "candidate_partition_fingerprints": [], - "recent_range_history": [], - "candidate_model": {}, - } - - monkeypatch.setattr(api_server, "_load_local_module", lambda _name: FakeDynamicModule) - - result = _dynamic_handler(monkeypatch)._compute_dynamic_rectification_opportunities( - _dynamic_base() + } ) - - assert result["success"] is True - assert result["endpoint"] == "dynamic_rectification_opportunities" - assert captured[0]["as_of_date"] == "2026-07-18" - assert captured[0]["lat"] == 31.23 - - -def test_dynamic_opportunities_api_rejects_missing_clock_and_untrusted_fields(monkeypatch) -> None: - handler = _dynamic_handler(monkeypatch) - missing_date = _dynamic_base() - del missing_date["as_of_date"] - with pytest.raises(BadRequest, match="as_of_date"): - handler._compute_dynamic_rectification_opportunities(missing_date) - - with pytest.raises(BadRequest, match="unsupported dynamic rectification opportunity field"): - handler._compute_dynamic_rectification_opportunities( - {**_dynamic_base(), "confidence": "high"} - ) - - with pytest.raises(BadRequest, match="recent_ranges"): - handler._compute_dynamic_rectification_opportunities( - {**_dynamic_base(), "recent_ranges": [{"start_time": "05:30", "extra": "05:33"}]} - ) - - with pytest.raises(BadRequest, match="partition evidence"): - handler._compute_dynamic_rectification_opportunities( - {**_dynamic_base(), "evidence": [{"kind": "unknown"}]} - ) - - for field in ("lat", "lon", "tz"): - missing_location = _dynamic_base() - del missing_location[field] - with pytest.raises(BadRequest, match=field): - handler._compute_dynamic_rectification_opportunities(missing_location) - - -def test_dynamic_score_api_rejects_client_option_ids_before_scoring(monkeypatch) -> None: - with pytest.raises(BadRequest, match="option_id"): - _dynamic_handler(monkeypatch)._compute_dynamic_rectification_score( - { - "birth_date": "1990-01-01", - "start_time": "05:30", - "end_time": "05:33", - "lat": 31.23, - "lon": 121.47, - "tz": 8.0, - "choice_evidence": [{"option_id": "client-owned"}], - } - ) - - -def test_dynamic_score_api_returns_versioned_candidate_result(monkeypatch) -> None: - class FakeDynamicModule: - @staticmethod - def score_choice_evidence(_payload: dict) -> dict: - return { - "result_id": "result-1", - "confidence": "low", - "can_apply": False, - "winning_segment": None, - "event_count": 0, - "domain_count": 0, - "top_score": 0.0, - "second_score": 0.0, - "margin_percent": 0.0, - "reasons": ["insufficient_effective_evidence"], - "evidence": [], - "algorithm_version": "birth-time-choice-scoring-v2", - "evidence_mode": "dynamic_choice", - "effective_answer_count": 0, - "dimension_count": 0, - } - - monkeypatch.setattr(api_server, "_load_local_module", lambda _name: FakeDynamicModule) - - result = _dynamic_handler(monkeypatch)._compute_dynamic_rectification_score( + return _handler()._compute_active_rectification_score( { - "birth_date": "1990-01-01", - "start_time": "05:30", - "end_time": "05:33", - "lat": 31.23, - "lon": 121.47, - "tz": 8.0, - "choice_evidence": [], + "questionnaire": questionnaire, + "answers": { + "education_environment_shift": "A", + "residence_relocation_shift": "B", + "relationship_or_partner_entry": "D", + "career_responsibility_pressure": "A", + "research_tool_expression_shift": "C", + }, } ) - assert result["success"] is True - assert result["endpoint"] == "dynamic_rectification_score" - assert result["algorithm_version"] == "birth-time-choice-scoring-v2" + +def test_rectification_score_exposes_narayana_cross_score_red() -> None: + scored = _answered_rectification_score() + + assert scored["candidate_cluster_rankings"] + assert all( + "narayana_cross_score" in candidate + for candidate in scored["candidate_cluster_rankings"] + ) + + +def test_rectification_technique_audit_mentions_narayana_red() -> None: + scored = _answered_rectification_score() + + audit_rows = scored["technique_audit_table"] + assert any( + row.get("technique") == "Narayana Dasha Rectification" + and row.get("status") in {"used", "partial"} + for row in audit_rows + ) + + +def test_narayana_conflict_downgrades_without_replacing_vimshottari_red() -> None: + scored = _handler()._compute_active_rectification_score( + { + "questionnaire": { + "questions": [ + { + "id": "career_responsibility_pressure", + "round": 1, + "scoring_map": { + "A": { + "cluster": "middle_candidate_cluster", + "points": 9, + } + }, + } + ] + }, + "answers": {"career_responsibility_pressure": "A"}, + "narayana_cross_scores": { + "early_candidate_cluster": 10, + "middle_candidate_cluster": -10, + }, + } + ) + + top = scored["candidate_cluster_rankings"][0] + assert top["cluster"] == "middle_candidate_cluster" + assert top["claim_status"] == "candidate" + assert top["confidence_cap"] == "low" + assert top["conflict_policy"] == "downgrade_without_replacement" + + +def test_rectification_claim_remains_candidate_not_birth_time_truth_red() -> None: + scored = _answered_rectification_score() + + assert scored["claim_status"] == "candidate" + assert scored["truth_status"] != "birth_time_truth" + + +def test_rectification_score_exposes_jaimini_karaka_cross_score_red() -> None: + scored = _answered_rectification_score() + + assert scored["candidate_cluster_rankings"] + assert all( + "jaimini_karaka_cross_score" in candidate + for candidate in scored["candidate_cluster_rankings"] + ) + + +def test_rectification_technique_audit_mentions_jaimini_karaka_red() -> None: + scored = _answered_rectification_score() + + audit_rows = scored["technique_audit_table"] + assert any( + row.get("technique") == "Jaimini Karaka Rectification" + and row.get("status") == "partial" + for row in audit_rows + ) + + +def test_jaimini_karaka_conflict_downgrades_without_replacing_primary_rank_red() -> None: + scored = _handler()._compute_active_rectification_score( + { + "questionnaire": { + "questions": [ + { + "id": "career_responsibility_pressure", + "round": 1, + "scoring_map": { + "A": { + "cluster": "middle_candidate_cluster", + "points": 9, + } + }, + } + ] + }, + "answers": {"career_responsibility_pressure": "A"}, + "jaimini_karaka_cross_scores": { + "early_candidate_cluster": 10, + "middle_candidate_cluster": -10, + }, + } + ) + + top = scored["candidate_cluster_rankings"][0] + assert top["cluster"] == "middle_candidate_cluster" + assert top["claim_status"] == "candidate" + assert top["confidence_cap"] == "low" + assert "jaimini_karaka" in top["downgrade_reasons"] + + +def test_rectification_score_exposes_vimsopaka_avastha_cross_score_red() -> None: + scored = _answered_rectification_score() + + assert scored["candidate_cluster_rankings"] + assert all( + "vimsopaka_avastha_cross_score" in candidate + for candidate in scored["candidate_cluster_rankings"] + ) + + +def test_rectification_technique_audit_mentions_vimsopaka_avastha_red() -> None: + scored = _answered_rectification_score() + + audit_rows = scored["technique_audit_table"] + assert any( + row.get("technique") == "Vimsopaka Avastha Rectification" + and row.get("status") == "partial" + for row in audit_rows + ) + + +def test_vimsopaka_avastha_conflict_downgrades_without_replacing_primary_rank_red() -> None: + scored = _handler()._compute_active_rectification_score( + { + "questionnaire": { + "questions": [ + { + "id": "career_responsibility_pressure", + "round": 1, + "scoring_map": { + "A": { + "cluster": "middle_candidate_cluster", + "points": 9, + } + }, + } + ] + }, + "answers": {"career_responsibility_pressure": "A"}, + "vimsopaka_avastha_cross_scores": { + "early_candidate_cluster": 10, + "middle_candidate_cluster": -10, + }, + } + ) + + top = scored["candidate_cluster_rankings"][0] + assert top["cluster"] == "middle_candidate_cluster" + assert top["claim_status"] == "candidate" + assert top["confidence_cap"] == "low" + assert "vimsopaka_avastha" in top["downgrade_reasons"] + + +def test_rectification_score_exposes_shadbala_av_observation_score_red() -> None: + scored = _answered_rectification_score() + + assert scored["candidate_cluster_rankings"] + assert all( + "shadbala_av_observation_score" in candidate + for candidate in scored["candidate_cluster_rankings"] + ) + assert scored["formula_unit_parity_status"] == "partial" + + +def test_rectification_technique_audit_mentions_shadbala_av_low_weight_red() -> None: + scored = _answered_rectification_score() + + audit_rows = scored["technique_audit_table"] + assert any( + row.get("technique") == "Shadbala Ashtakavarga Rectification" + and row.get("status") == "partial_observation" + and row.get("weight_policy") == "low_weight_only" + for row in audit_rows + ) + + +def test_shadbala_av_conflict_downgrades_without_replacing_primary_rank_red() -> None: + scored = _handler()._compute_active_rectification_score( + { + "questionnaire": { + "questions": [ + { + "id": "career_responsibility_pressure", + "round": 1, + "scoring_map": { + "A": { + "cluster": "middle_candidate_cluster", + "points": 9, + } + }, + } + ] + }, + "answers": {"career_responsibility_pressure": "A"}, + "shadbala_av_observation_scores": { + "early_candidate_cluster": 10, + "middle_candidate_cluster": -10, + }, + } + ) + + top = scored["candidate_cluster_rankings"][0] + assert top["cluster"] == "middle_candidate_cluster" + assert top["claim_status"] == "candidate" + assert top["confidence_cap"] == "low" + assert "shadbala_av" in top["downgrade_reasons"] + + +def test_rectification_score_exposes_gochara_observation_score_red() -> None: + scored = _answered_rectification_score() + + assert scored["candidate_cluster_rankings"] + assert all( + "gochara_transit_observation_score" in candidate + for candidate in scored["candidate_cluster_rankings"] + ) + assert scored["timing_claim_status"] == "exploratory_unvalidated" + + +def test_rectification_technique_audit_mentions_gochara_holdout_gate_red() -> None: + scored = _answered_rectification_score() + + audit_rows = scored["technique_audit_table"] + assert any( + row.get("technique") == "Gochara Transit Rectification" + and row.get("status") == "blocked_from_verified_timing" + and row.get("holdout_gate") == "negative_holdout_required" + for row in audit_rows + ) + + +def test_gochara_conflict_downgrades_without_verified_timing_claim_red() -> None: + scored = _handler()._compute_active_rectification_score( + { + "questionnaire": { + "questions": [ + { + "id": "career_responsibility_pressure", + "round": 1, + "scoring_map": { + "A": { + "cluster": "middle_candidate_cluster", + "points": 9, + } + }, + } + ] + }, + "answers": {"career_responsibility_pressure": "A"}, + "gochara_transit_observation_scores": { + "early_candidate_cluster": 10, + "middle_candidate_cluster": -10, + }, + } + ) + + top = scored["candidate_cluster_rankings"][0] + assert top["cluster"] == "middle_candidate_cluster" + assert top["claim_status"] == "candidate" + assert top["confidence_cap"] == "low" + assert "gochara_transit" in top["downgrade_reasons"] + assert scored["timing_claim_status"] == "exploratory_unvalidated" diff --git a/tests/test_active_rectification_questions.py b/tests/test_active_rectification_questions.py index f5dc044b..1c029c2c 100644 --- a/tests/test_active_rectification_questions.py +++ b/tests/test_active_rectification_questions.py @@ -43,34 +43,7 @@ def test_active_rectification_scores_answers_and_selects_next_round() -> None: assert scored["next_round"] == 2 assert scored["next_round_questions"] assert scored["candidate_cluster_rankings"][0]["score"] > scored["candidate_cluster_rankings"][-1]["score"] - assert "final rectification requires scoring answers against actual candidate chart differences" in scored["boundary"] - - -def test_active_rectification_scores_legacy_questions_missing_scoring_maps() -> None: - report = build_questionnaire("1955-02-24 19:15", uncertainty_minutes=30) - legacy_questionnaire = { - "questions": [ - { - "id": question["id"], - "prompt": question["prompt"], - "options": question["options"], - } - for question in report["questions"] - ] - } - - scored = score_answers( - legacy_questionnaire, - { - "education_environment_shift": "A", - "residence_relocation_shift": "A", - "relationship_or_partner_entry": "B", - }, - ) - - assert scored["answered_count"] == 3 - assert scored["invalid_answers"] == [] - assert scored["candidate_cluster_rankings"] + assert "does not convert candidates into birth-time truth" in scored["boundary"] def test_active_rectification_recasts_candidate_vargas_when_location_is_available() -> None: @@ -95,23 +68,3 @@ def test_active_rectification_recasts_candidate_vargas_when_location_is_availabl assert sample["arudha"]["UL"]["sign"] assert sample["kp_cusps"]["house_7"]["sub_lord"] assert sample["kp_cusps"]["house_10"]["sub_sub_lord"] - - -def test_candidate_recast_contains_all_evidence_domain_vargas(monkeypatch) -> None: - report = build_questionnaire( - "1993-04-17 14:30", 30, 30, - lat=31.2304, lon=121.4737, tz=8, - ) - sample = report["candidate_scan"]["samples"][0] - varga_lagna = sample["varga_lagna"] - expected_legacy_keys = { - "D4": "D4_Turyamsa", - "D9": "D9_Navamsa", - "D10": "D10_Dasamsa", - "D24": "D24_Siddhamsa", - "D30": "D30_Trimsamsa", - } - assert set(expected_legacy_keys).issubset(varga_lagna) - for alias, legacy_key in expected_legacy_keys.items(): - assert varga_lagna[alias]["sign"] - assert varga_lagna[alias] == varga_lagna[legacy_key] diff --git a/tests/test_rectification_missing_layer_integration_plan.py b/tests/test_rectification_missing_layer_integration_plan.py new file mode 100644 index 00000000..1463fce9 --- /dev/null +++ b/tests/test_rectification_missing_layer_integration_plan.py @@ -0,0 +1,49 @@ +from __future__ import annotations + +import json +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +PLAN = ROOT / "references/oracle/rectification_missing_layer_integration_plan_2026_07_19.json" + + +def test_rectification_plan_covers_all_missing_layers() -> None: + data = json.loads(PLAN.read_text(encoding="utf-8")) + assert data["scope"] == "rectification_missing_layer_integration_plan" + assert data["status"] == "implementation_plan_v1" + assert data["production_tuning_allowed"] is False + assert data["claim_boundary"] == "candidate_rectification_not_birth_time_truth" + layers = {row["layer_id"] for row in data["layers"]} + assert layers == { + "narayana_dasha_cross_score", + "jaimini_karaka_sensitivity", + "shadbala_av_delta_score", + "vimsopaka_avastha_state_score", + "gochara_transit_trigger_score", + } + + +def test_rectification_plan_has_tests_and_gate_for_each_layer() -> None: + data = json.loads(PLAN.read_text(encoding="utf-8")) + allowed_statuses = { + "partial_runtime_cross_gate", + "partial_observation_low_weight_gate", + "partial_observation_holdout_blocked", + } + for row in data["layers"]: + assert row["implementation_status"] in allowed_statuses + assert row["entry_gate"] + assert row["test_targets"] + assert row["claim_boundary"] + assert row["output_status"] in {"exploratory", "exploratory_observation_only"} + + +def test_rectification_plan_prioritizes_safe_order() -> None: + data = json.loads(PLAN.read_text(encoding="utf-8")) + assert [row["layer_id"] for row in data["layers"]] == [ + "narayana_dasha_cross_score", + "jaimini_karaka_sensitivity", + "vimsopaka_avastha_state_score", + "shadbala_av_delta_score", + "gochara_transit_trigger_score", + ] diff --git a/tests/test_skill_truth_overlay_and_rectification_audit.py b/tests/test_skill_truth_overlay_and_rectification_audit.py new file mode 100644 index 00000000..af0cbc16 --- /dev/null +++ b/tests/test_skill_truth_overlay_and_rectification_audit.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +import json +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +OVERLAY = ROOT / "references/oracle/skill_truth_overlay_2026_07_19.json" +RECT = ROOT / "references/oracle/rectification_technique_usage_audit_2026_07_19.json" + + +def test_skill_truth_overlay_corrects_overclaimed_registry_items() -> None: + data = json.loads(OVERLAY.read_text(encoding="utf-8")) + assert data["scope"] == "skill_truth_overlay" + assert data["status"] == "truth_overlay_v1" + corrected = {row["technique_id"]: row["corrected_status"] for row in data["overrides"]} + assert corrected["kp_system"] == "reference_only" + assert corrected["muhurta"] == "reference_only" + assert corrected["gochara_event_timing"] == "reference_only" + assert corrected["sahams"] == "blocked" + assert corrected["sphuta_trisphuta_family"] == "blocked" + assert corrected["tajika_yogas"] == "partial" + + +def test_rectification_audit_identifies_used_and_guarded_layers() -> None: + data = json.loads(RECT.read_text(encoding="utf-8")) + used = {row["technique_id"]: row for row in data["used_layers"]} + assert {"candidate_time_sweep", "vimshottari_event_scoring", "varga_change_scoring", "d60_late_reference"}.issubset(used) + assert { + "narayana_dasha_rectification", + "jaimini_karaka_rectification", + "shadbala_av_rectification", + "vimsopaka_avastha_rectification", + "gochara_transit_rectification", + }.issubset(used) + assert used["shadbala_av_rectification"]["status"] == "partial_observation_low_weight_gate" + assert used["gochara_transit_rectification"]["status"] == "partial_observation_holdout_blocked" + assert data["claim_boundary"] == "rectification_candidate_scoring_not_birth_time_truth" + + +def test_rectification_audit_keeps_all_partial_layers_guarded() -> None: + data = json.loads(RECT.read_text(encoding="utf-8")) + assert data["not_yet_used_layers"] == [] + for row in data["used_layers"]: + assert row["claim_boundary"] + guarded = [row for row in data["used_layers"] if "rectification" in row["technique_id"]] + assert guarded + assert all("truth" in row["claim_boundary"] or "proof" in row["claim_boundary"] or "correction" in row["claim_boundary"] or "confidence" in row["claim_boundary"] for row in guarded)