feat: sync guarded rectification gates

This commit is contained in:
732642856
2026-07-19 16:09:24 +08:00
parent 0bb7dbfe42
commit 352809fa82
9 changed files with 761 additions and 230 deletions
@@ -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."
}
]
}
@@ -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"
]
}
@@ -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": []
}
+218 -13
View File
@@ -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")
+9 -1
View File
@@ -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',
+292 -166
View File
@@ -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"
+1 -48
View File
@@ -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]
@@ -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",
]
@@ -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)