feat: enforce commercial rectification evidence contracts
* feat: enforce precise timing output contract * fix: recognize package imports in fragment audit * test: make workflow stream contract formatting-independent * fix: preserve VedAstro evidence across async workflows * feat: enforce commercial technique truth contract * feat: add rectification technique receipt * feat: extend rectification event evidence * feat: score rectification arudha evidence * feat: gate high rigor rectification confirmation * feat: add controlled transit to rectification * feat: include d11 in rectification finance scoring * feat: add ashtakavarga rectification auxiliary * feat: show rectification technique receipt * feat: use verified shadbala components in rectification * fix: trace transitive script references in fragment audit * feat: run request-level rectification parity packet
This commit is contained in:
@@ -32,16 +32,22 @@ if str(SCRIPTS) not in sys.path:
|
||||
|
||||
import dasha_analyzer # noqa: E402
|
||||
import domain_calculation_service # noqa: E402
|
||||
import ashtakavarga # noqa: E402
|
||||
import divisional_charts_extended # noqa: E402
|
||||
import functional_benefics # noqa: E402
|
||||
import jaimini # noqa: E402
|
||||
import shadbala # noqa: E402
|
||||
import narayana_dasha # noqa: E402
|
||||
import varga # noqa: E402
|
||||
|
||||
DomainConfig = tuple[str, tuple[int, ...]]
|
||||
DomainConfig = tuple[tuple[str, ...], tuple[int, ...]]
|
||||
DOMAIN_CONFIG: Final[dict[EventDomain, DomainConfig]] = {
|
||||
"education": ("D24", (4, 5, 9)),
|
||||
"relocation": ("D4", (4, 12)),
|
||||
"relationship": ("D9", (7,)),
|
||||
"career": ("D10", (10,)),
|
||||
"health_pressure": ("D30", (6, 8, 12)),
|
||||
"education": (("D24",), (4, 5, 9)),
|
||||
"relocation": (("D4",), (4, 12)),
|
||||
"relationship": (("D9",), (7,)),
|
||||
"career": (("D10",), (10,)),
|
||||
"finance": (("D2", "D11"), (2, 11)),
|
||||
"health_pressure": (("D30",), (6, 8, 12)),
|
||||
}
|
||||
|
||||
|
||||
@@ -77,7 +83,7 @@ def _active_vimshottari(
|
||||
birth_date: str,
|
||||
moon_longitude: float,
|
||||
event_at: datetime,
|
||||
) -> tuple[str, str]:
|
||||
) -> tuple[str, str, str]:
|
||||
nakshatra, progress, _ = dasha_analyzer.lon_to_nakshatra(moon_longitude)
|
||||
timeline, _, _, _ = dasha_analyzer.build_dasha_timeline(
|
||||
birth_date,
|
||||
@@ -89,7 +95,11 @@ def _active_vimshottari(
|
||||
dasha_analyzer.build_antardasha(major),
|
||||
event_at,
|
||||
)
|
||||
return str(major["lord"]), str(minor["lord"])
|
||||
pratyantar = dasha_analyzer.find_current_sub(
|
||||
dasha_analyzer.build_antardasha(minor),
|
||||
event_at,
|
||||
)
|
||||
return str(major["lord"]), str(minor["lord"]), str(pratyantar["lord"])
|
||||
|
||||
|
||||
def _active_narayana(
|
||||
@@ -116,6 +126,20 @@ def _varga_chart(charts: dict, prefix: str) -> dict | None:
|
||||
)
|
||||
|
||||
|
||||
def _d11_chart(planet_longitudes: dict[str, float], ascendant_longitude: float) -> dict:
|
||||
"""Adapt the repository's Rudramsa implementation to the event-score shape."""
|
||||
raw = divisional_charts_extended.DivisionalChartsCalculator().calculate_all_vargas(
|
||||
planet_longitudes, ascendant_longitude,
|
||||
)["Rudramsa"]
|
||||
return {
|
||||
"Ascendant": {"sign_idx": raw["ascendant"]["sign_index"]},
|
||||
**{
|
||||
planet: {"sign_idx": value["sign_index"]}
|
||||
for planet, value in raw["planets"].items()
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _relative_house(sign_index: int, ascendant_index: int) -> int:
|
||||
return (sign_index - ascendant_index) % 12 + 1
|
||||
|
||||
@@ -149,20 +173,27 @@ def _score_event(
|
||||
candidate_time: str,
|
||||
event: LifeEvent,
|
||||
natal_chart: dict,
|
||||
varga_chart: dict,
|
||||
vimshottari: tuple[str, str],
|
||||
varga_charts: list[dict],
|
||||
vimshottari: tuple[str, str, str],
|
||||
narayana: tuple[int | None, int | None],
|
||||
arudha_padas: dict,
|
||||
) -> CandidateEvidence:
|
||||
_, target_houses = DOMAIN_CONFIG[event["domain"]]
|
||||
ascendant_index = int(natal_chart["ascendant"]["lon"] // 30)
|
||||
target_lords = _house_lords(ascendant_index, target_houses)
|
||||
major_lord, minor_lord = vimshottari
|
||||
functional = functional_benefics.derive_functional_benefic_malefic(
|
||||
natal_chart["ascendant"].get("sign")
|
||||
)
|
||||
functional_benefics_set = set(functional.get("functional_benefics") or [])
|
||||
functional_malefics_set = set(functional.get("functional_malefics") or [])
|
||||
major_lord, minor_lord, pratyantar_lord = vimshottari
|
||||
rules: list[str] = []
|
||||
points = 0.0
|
||||
|
||||
for lord, weight, label in (
|
||||
(major_lord, 2.0, "vim_md"),
|
||||
(minor_lord, 1.5, "vim_ad"),
|
||||
(pratyantar_lord, 0.75, "vim_pd"),
|
||||
):
|
||||
if _planet_house(natal_chart, lord) in target_houses:
|
||||
rules.append(f"{label}_domain_house")
|
||||
@@ -170,9 +201,16 @@ def _score_event(
|
||||
if lord in target_lords:
|
||||
rules.append(f"{label}_domain_lord")
|
||||
points += weight
|
||||
if _varga_house(varga_chart, lord) in target_houses:
|
||||
rules.append(f"{label}_domain_varga")
|
||||
points += weight / 2
|
||||
for varga_chart in varga_charts:
|
||||
if _varga_house(varga_chart, lord) in target_houses:
|
||||
rules.append(f"{label}_domain_varga")
|
||||
points += weight / (2 * len(varga_charts))
|
||||
if lord in functional_benefics_set:
|
||||
rules.append(f"{label}_functional_benefic_auxiliary")
|
||||
points += 0.2
|
||||
elif lord in functional_malefics_set:
|
||||
rules.append(f"{label}_functional_malefic_auxiliary")
|
||||
points -= 0.1
|
||||
|
||||
for sign_index, weight, label in (
|
||||
(narayana[0], 2.0, "narayana_md"),
|
||||
@@ -181,6 +219,17 @@ def _score_event(
|
||||
if sign_index is not None and _relative_house(sign_index, ascendant_index) in target_houses:
|
||||
rules.append(f"{label}_domain_house")
|
||||
points += weight
|
||||
arudha_keys = ("A7", "UL") if event["domain"] == "relationship" else ("A10",) if event["domain"] == "career" else ()
|
||||
arudha_signs = {
|
||||
value.get("sign_idx") for key in arudha_keys
|
||||
if isinstance((value := arudha_padas.get(key)), dict) and isinstance(value.get("sign_idx"), int)
|
||||
}
|
||||
if arudha_signs:
|
||||
for lord, label in ((major_lord, "vim_md"), (minor_lord, "vim_ad"), (pratyantar_lord, "vim_pd")):
|
||||
planet = natal_chart.get("planets", {}).get(lord) or {}
|
||||
if isinstance(planet.get("lon"), (int, float)) and int(planet["lon"] // 30) in arudha_signs:
|
||||
rules.append(f"{label}_arudha_auxiliary")
|
||||
points += 0.35
|
||||
|
||||
weighted_points = round(points * precision_weight(event["precision"]), 4)
|
||||
return {
|
||||
@@ -192,6 +241,76 @@ def _score_event(
|
||||
}
|
||||
|
||||
|
||||
def _controlled_transit_rules(
|
||||
request: RectificationEventRequest,
|
||||
event: LifeEvent,
|
||||
natal_ascendant_index: int,
|
||||
target_houses: tuple[int, ...],
|
||||
) -> list[str]:
|
||||
"""Use only Jupiter/Saturn and only day/month dated events as a weak check."""
|
||||
if event["precision"] == "year":
|
||||
return []
|
||||
event_at = _event_datetime(event)
|
||||
transit_chart = domain_calculation_service.compute_chart({
|
||||
"year": event_at.year, "month": event_at.month, "day": event_at.day,
|
||||
"hour": 12, "minute": 0, "lat": request["lat"], "lon": request["lon"],
|
||||
"tz": request["tz"], "ayanamsa": "lahiri", "node_mode": "true",
|
||||
})
|
||||
rules: list[str] = []
|
||||
for planet in ("Jupiter", "Saturn"):
|
||||
item = transit_chart.get("planets", {}).get(planet) or {}
|
||||
if isinstance(item.get("lon"), (int, float)) and _relative_house(int(item["lon"] // 30), natal_ascendant_index) in target_houses:
|
||||
rules.append(f"controlled_transit_{planet.lower()}_domain_house")
|
||||
return rules
|
||||
|
||||
|
||||
def _ashtakavarga_auxiliary(natal_chart: dict, ascendant_index: int, target_houses: tuple[int, ...]) -> tuple[list[str], float]:
|
||||
"""Return a bounded SAV consistency adjustment, never a standalone trigger."""
|
||||
result = ashtakavarga.calc_ashtakavarga(natal_chart.get("planets", {}), ascendant_index)
|
||||
if not result.get("all_bav_valid") or not (result.get("sav") or {}).get("valid"):
|
||||
return [], 0.0
|
||||
house_scores = result.get("house_scores_full") or {}
|
||||
values = [house_scores.get(f"house_{house}", {}).get("sav_score") for house in target_houses]
|
||||
numeric = [float(value) for value in values if isinstance(value, (int, float))]
|
||||
if not numeric:
|
||||
return [], 0.0
|
||||
average = sum(numeric) / len(numeric)
|
||||
if average >= 32:
|
||||
return ["ashtakavarga_target_house_support_auxiliary"], 0.2
|
||||
if average <= 24:
|
||||
return ["ashtakavarga_target_house_pressure_auxiliary"], -0.1
|
||||
return [], 0.0
|
||||
|
||||
|
||||
def _shadbala_verified_components_auxiliary(natal_chart: dict, birth_hour: float, dasha_lords: tuple[str, str, str]) -> tuple[list[str], float]:
|
||||
"""Use only Sthana/Drik/Naisargika, whose oracle comparison is already matched."""
|
||||
planets = natal_chart.get("planets", {})
|
||||
sun = planets.get("Sun") or {}
|
||||
moon = planets.get("Moon") or {}
|
||||
if not isinstance(sun.get("lon"), (int, float)) or not isinstance(moon.get("lon"), (int, float)):
|
||||
return [], 0.0
|
||||
result = shadbala.calc_shadbala(
|
||||
planets, str(natal_chart["ascendant"].get("sign") or "Aries"), birth_hour,
|
||||
float(sun["lon"]), float(moon["lon"]),
|
||||
)
|
||||
values = {
|
||||
planet: float((row.get("sthana_bala") or {}).get("total", 0)) + float(row.get("drik_bala", 0)) + float(row.get("naisargika_bala", 0))
|
||||
for planet, row in (result.get("planets") or {}).items()
|
||||
}
|
||||
if not values:
|
||||
return [], 0.0
|
||||
baseline = sum(values.values()) / len(values)
|
||||
active = [values[lord] for lord in dasha_lords if lord in values]
|
||||
if not active:
|
||||
return [], 0.0
|
||||
average = sum(active) / len(active)
|
||||
if average > baseline:
|
||||
return ["shadbala_sthana_drik_naisargika_support_auxiliary"], 0.1
|
||||
if average < baseline:
|
||||
return ["shadbala_sthana_drik_naisargika_pressure_auxiliary"], -0.05
|
||||
return [], 0.0
|
||||
|
||||
|
||||
def _candidate_row(
|
||||
request: RectificationEventRequest,
|
||||
candidate_at: datetime,
|
||||
@@ -215,21 +334,23 @@ def _candidate_row(
|
||||
}
|
||||
ascendant_longitude = float(chart["ascendant"]["lon"])
|
||||
ascendant_index = int(ascendant_longitude // 30)
|
||||
arudha_padas = (jaimini.calc_arudha_padas(ascendant_index, planet_longitudes).get("padas") or {})
|
||||
charts = varga.calc_all_vargas(
|
||||
planet_longitudes,
|
||||
ascendant_longitude,
|
||||
divisions=[4, 9, 10, 24, 30],
|
||||
divisions=[2, 4, 9, 10, 24, 30],
|
||||
)
|
||||
d11_chart = _d11_chart(planet_longitudes, ascendant_longitude)
|
||||
moon_longitude = planet_longitudes["Moon"]
|
||||
evidence: list[CandidateEvidence] = []
|
||||
missing_layers: list[str] = []
|
||||
|
||||
for event in request["events"]:
|
||||
event_at = _event_datetime(event)
|
||||
prefix, _ = DOMAIN_CONFIG[event["domain"]]
|
||||
domain_varga = _varga_chart(charts, prefix)
|
||||
if domain_varga is None:
|
||||
missing_layers.append(prefix)
|
||||
prefixes, _ = DOMAIN_CONFIG[event["domain"]]
|
||||
domain_vargas = [d11_chart if prefix == "D11" else _varga_chart(charts, prefix) for prefix in prefixes]
|
||||
if any(chart is None for chart in domain_vargas):
|
||||
missing_layers.extend(prefixes)
|
||||
continue
|
||||
vimshottari = _active_vimshottari(request["birth_date"], moon_longitude, event_at)
|
||||
narayana = _active_narayana(
|
||||
@@ -242,10 +363,25 @@ def _candidate_row(
|
||||
candidate_time=candidate_at.strftime("%H:%M"),
|
||||
event=event,
|
||||
natal_chart=chart,
|
||||
varga_chart=domain_varga,
|
||||
varga_charts=[chart for chart in domain_vargas if chart is not None],
|
||||
vimshottari=vimshottari,
|
||||
narayana=narayana,
|
||||
arudha_padas=arudha_padas,
|
||||
))
|
||||
transit_rules = _controlled_transit_rules(request, event, ascendant_index, DOMAIN_CONFIG[event["domain"]][1])
|
||||
if transit_rules:
|
||||
evidence[-1]["rule_ids"].extend(transit_rules)
|
||||
evidence[-1]["points"] = round(evidence[-1]["points"] + 0.25 * len(transit_rules) * precision_weight(event["precision"]), 4)
|
||||
av_rules, av_points = _ashtakavarga_auxiliary(chart, ascendant_index, DOMAIN_CONFIG[event["domain"]][1])
|
||||
if av_rules:
|
||||
evidence[-1]["rule_ids"].extend(av_rules)
|
||||
evidence[-1]["points"] = round(evidence[-1]["points"] + av_points * precision_weight(event["precision"]), 4)
|
||||
shadbala_rules, shadbala_points = _shadbala_verified_components_auxiliary(
|
||||
chart, candidate_at.hour + candidate_at.minute / 60, vimshottari,
|
||||
)
|
||||
if shadbala_rules:
|
||||
evidence[-1]["rule_ids"].extend(shadbala_rules)
|
||||
evidence[-1]["points"] = round(evidence[-1]["points"] + shadbala_points * precision_weight(event["precision"]), 4)
|
||||
|
||||
return {
|
||||
"time": candidate_at.strftime("%H:%M"),
|
||||
|
||||
@@ -18,6 +18,7 @@ EventDomain = Literal[
|
||||
"relocation",
|
||||
"relationship",
|
||||
"career",
|
||||
"finance",
|
||||
"health_pressure",
|
||||
]
|
||||
Confidence = Literal["low", "medium", "high"]
|
||||
|
||||
@@ -179,6 +179,25 @@ def _kp_cusp_snapshot(chart: dict[str, Any]) -> dict[str, Any]:
|
||||
return snapshot
|
||||
|
||||
|
||||
def _prioritize_questions(questions: list[dict[str, Any]], scan: dict[str, Any]) -> tuple[list[dict[str, Any]], str]:
|
||||
"""Prefer questions whose declared layers actually differ in sampled candidates."""
|
||||
samples = scan.get("samples") or []
|
||||
if len(samples) < 2 or not all(isinstance(sample.get("varga_lagna"), dict) for sample in samples):
|
||||
return questions, "generic_fallback_missing_candidate_recast"
|
||||
changed: set[str] = set()
|
||||
for layer in ("D4", "D9", "D10", "D24", "D30"):
|
||||
values = {str((sample["varga_lagna"].get(layer) or {}).get("sign_idx")) for sample in samples}
|
||||
if len(values) > 1:
|
||||
changed.add(layer)
|
||||
for layer in ("A7", "A10", "UL"):
|
||||
values = {str(((sample.get("arudha") or {}).get(layer) or {}).get("sign_idx")) for sample in samples}
|
||||
if len(values) > 1:
|
||||
changed.add(layer)
|
||||
if not changed:
|
||||
return questions, "generic_fallback_no_sampled_difference"
|
||||
return sorted(questions, key=lambda question: (not bool(changed.intersection(question.get("sensitivity") or [])), question.get("round", 99))), "candidate_difference_ranked"
|
||||
|
||||
|
||||
def build_questionnaire(
|
||||
birth_time: str,
|
||||
uncertainty_minutes: int = 30,
|
||||
@@ -206,18 +225,16 @@ def build_questionnaire(
|
||||
"D": {"effect": "neutral", "cluster": "neutral", "points": 0},
|
||||
},
|
||||
})
|
||||
scan = _candidate_scan(
|
||||
_parse_time(birth_time), uncertainty_minutes, step_minutes,
|
||||
lat=lat, lon=lon, tz=tz, ayanamsa=ayanamsa,
|
||||
)
|
||||
questions, question_selection = _prioritize_questions(questions, scan)
|
||||
return {
|
||||
"scope": "active_birth_time_rectification_questionnaire",
|
||||
"schema_version": 1,
|
||||
"candidate_scan": _candidate_scan(
|
||||
_parse_time(birth_time),
|
||||
uncertainty_minutes,
|
||||
step_minutes,
|
||||
lat=lat,
|
||||
lon=lon,
|
||||
tz=tz,
|
||||
ayanamsa=ayanamsa,
|
||||
),
|
||||
"candidate_scan": scan,
|
||||
"question_selection": question_selection,
|
||||
"workflow": [
|
||||
"candidate_time_scan",
|
||||
"varga_arudha_kp_sensitivity_diff",
|
||||
@@ -231,7 +248,7 @@ def build_questionnaire(
|
||||
"2": "domain follow-up",
|
||||
"3": "fine confirmation",
|
||||
},
|
||||
"sensitivity_layers": ["D9", "D10", "D24", "D30", "D60", "D4", "UL", "A7", "A10", "KP_cusp", "Vimshottari", "Narayana", "Chara"],
|
||||
"sensitivity_layers": ["D9", "D10", "D24", "D30", "D60", "D4", "UL", "A7", "A10", "Vimshottari", "Narayana", "Chara"],
|
||||
"questions": questions,
|
||||
"boundary": "Question generation only; final rectification requires scoring answers against actual candidate chart differences.",
|
||||
}
|
||||
|
||||
@@ -88,6 +88,7 @@ QUESTION_TEMPLATES: Final[tuple[QuestionTemplate, ...]] = (
|
||||
QuestionTemplate("residence_relocation_shift", 1, "residence", ("D4", "12H", "Rahu/Ketu", "Transit"), "age_20_to_24", "20-24岁附近,是否有搬家、离乡、长期异地、住宿或居住结构变化?", "D4_relocation_cluster", "against_D4_relocation_cluster"),
|
||||
QuestionTemplate("relationship_or_partner_entry", 1, "relationship", ("D9", "UL", "A7", "7H"), "age_21_to_26", "21-26岁附近,是否有关系对象进入、关系断裂、暧昧升级或关系观明显转变?", "D9_UL_A7_cluster", "against_relationship_cluster"),
|
||||
QuestionTemplate("career_responsibility_pressure", 1, "career", ("D10", "A10", "Saturn", "10H"), "age_26_to_30", "26-30岁附近,是否有责任增加、合作压力、工作结构变化或长期压力阶段?", "D10_A10_saturn_cluster", "against_career_pressure_cluster"),
|
||||
QuestionTemplate("finance_resource_shift", 1, "finance", ("D2", "2H", "11H"), "resource_change_window", "是否有收入结构、重要资产、资助、负债或资源渠道发生明显变化的阶段?", "D2_resource_cluster", "against_D2_resource_cluster"),
|
||||
QuestionTemplate("research_tool_expression_shift", 1, "career_learning", ("D10", "D24", "Mercury", "A10"), "recent_three_years", "近三年是否明显进入写作、技术、系统化学习、工具搭建、内容表达、AI/研究类方向?", "Mercury_D24_A10_cluster", "against_learning_expression_cluster"),
|
||||
QuestionTemplate("health_crisis_or_low_period", 2, "health_pressure", ("D30", "6H", "8H", "Saturn/Mars"), "largest_pressure_window", "某个压力窗口附近,是否有健康、事故、低谷、睡眠/精神压力或身体负担明显阶段?", "D30_crisis_cluster", "against_D30_crisis_cluster"),
|
||||
QuestionTemplate("public_role_or_project_visibility", 2, "public_work", ("A10", "D10", "AmK", "Karakamsha"), "career_visibility_window", "某个事业窗口附近,是否有项目公开、作品产出、职位/身份变化或被他人看见的机会?", "A10_public_visibility_cluster", "against_A10_cluster"),
|
||||
|
||||
@@ -234,13 +234,39 @@ def source_referenced_scripts(*texts: str) -> set[str]:
|
||||
refs: set[str] = set()
|
||||
for path in SCRIPTS_DIR.glob("*.py"):
|
||||
stem = path.stem
|
||||
if re.search(rf"\b(import|from)\s+{re.escape(stem)}\b", combined):
|
||||
if re.search(rf"\b(import|from)\s+(?:scripts\.)?{re.escape(stem)}\b", combined):
|
||||
refs.add(path.name)
|
||||
if path.name in combined or stem in combined:
|
||||
refs.add(path.name)
|
||||
return refs
|
||||
|
||||
|
||||
def transitive_source_referenced_scripts(*texts: str) -> set[str]:
|
||||
"""Follow local script imports so indirect runtime modules are not fragments."""
|
||||
combined = "\n".join(texts)
|
||||
names = set(re.findall(r"(?:from|import)\s+(?:scripts\.)?([A-Za-z_][A-Za-z0-9_]*)", combined))
|
||||
names |= set(re.findall(r"_load_local_module\(['\"]([A-Za-z_][A-Za-z0-9_]*)['\"]\)", combined))
|
||||
referenced = {f"{name}.py" for name in names if (SCRIPTS_DIR / f"{name}.py").exists()}
|
||||
pending = list(referenced)
|
||||
visited: set[str] = set()
|
||||
while pending:
|
||||
filename = pending.pop()
|
||||
if filename in visited:
|
||||
continue
|
||||
visited.add(filename)
|
||||
path = SCRIPTS_DIR / filename
|
||||
if not path.exists():
|
||||
continue
|
||||
text = read_text(path)
|
||||
child_names = set(re.findall(r"(?:from|import)\s+(?:scripts\.)?([A-Za-z_][A-Za-z0-9_]*)", text))
|
||||
child_names |= set(re.findall(r"_load_local_module\(['\"]([A-Za-z_][A-Za-z0-9_]*)['\"]\)", text))
|
||||
for child in {f"{name}.py" for name in child_names if (SCRIPTS_DIR / f"{name}.py").exists()}:
|
||||
if child not in referenced:
|
||||
referenced.add(child)
|
||||
pending.append(child)
|
||||
return referenced
|
||||
|
||||
|
||||
def find_script_fragments(registry: dict[str, Any], frontend: dict[str, Any], test_text: str) -> dict[str, Any]:
|
||||
api_text = read_text(SCRIPTS_DIR / "jyotish_api_server.py")
|
||||
engine_text = read_text(SCRIPTS_DIR / "jyotish_engine.py")
|
||||
@@ -248,6 +274,7 @@ def find_script_fragments(registry: dict[str, Any], frontend: dict[str, Any], te
|
||||
referenced = set(SCRIPT_IGNORE)
|
||||
referenced |= registry_script_refs(registry)
|
||||
referenced |= source_referenced_scripts(api_text, engine_text, app_text, test_text)
|
||||
referenced |= transitive_source_referenced_scripts(api_text, engine_text, app_text, test_text)
|
||||
candidates = []
|
||||
for path in sorted(SCRIPTS_DIR.glob("*.py")):
|
||||
if path.name in referenced:
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
"""Load and apply commercial claim boundaries for restricted techniques.
|
||||
|
||||
This is deliberately a product-owned status contract. It never imports a research
|
||||
workspace or reproduces research calculations.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
OVERLAY_PATH = (
|
||||
Path(__file__).resolve().parents[1]
|
||||
/ "references"
|
||||
/ "oracle"
|
||||
/ "commercial_skill_truth_overlay.v1.json"
|
||||
)
|
||||
ORACLE_DIR = OVERLAY_PATH.parent
|
||||
TECHNIQUE_TRUTH_IDS = (
|
||||
"kp_system",
|
||||
"muhurta",
|
||||
"gochara_event_timing",
|
||||
"sahams",
|
||||
"sphuta_trisphuta_family",
|
||||
"tajika_yogas",
|
||||
"conception_chart",
|
||||
"relationship_combinations",
|
||||
)
|
||||
_BLOCKED_STATUSES = {"blocked", "research_only_blocked"}
|
||||
|
||||
|
||||
def load_commercial_skill_truth() -> dict[str, Any]:
|
||||
"""Return the local, public-safe commercial status contract."""
|
||||
with OVERLAY_PATH.open(encoding="utf-8") as handle:
|
||||
payload = json.load(handle)
|
||||
techniques = payload.get("techniques")
|
||||
if not isinstance(techniques, list):
|
||||
raise ValueError("commercial technique truth overlay must contain techniques")
|
||||
by_id = {item.get("technique_id"): item for item in techniques if isinstance(item, dict)}
|
||||
if set(by_id) != set(TECHNIQUE_TRUTH_IDS):
|
||||
raise ValueError("commercial technique truth overlay has an unexpected technique set")
|
||||
return payload
|
||||
|
||||
|
||||
def apply_commercial_skill_truth(result: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Attach immutable claim limits to a server workflow receipt."""
|
||||
enriched = copy.deepcopy(result)
|
||||
techniques = load_commercial_skill_truth()["techniques"]
|
||||
blocked = [item["technique_id"] for item in techniques if item["status"] in _BLOCKED_STATUSES]
|
||||
restricted = [item["technique_id"] for item in techniques]
|
||||
enriched["technique_truth"] = {
|
||||
"status": "restricted",
|
||||
"techniques": techniques,
|
||||
"blocked_techniques": blocked,
|
||||
"reference_only_techniques": [
|
||||
item["technique_id"] for item in techniques if item["status"] == "reference_only"
|
||||
],
|
||||
"partial_techniques": [
|
||||
item["technique_id"]
|
||||
for item in techniques
|
||||
if item["status"] in {"partial", "partial_registry_only"}
|
||||
],
|
||||
}
|
||||
answer_policy = enriched.setdefault("answer_policy", {})
|
||||
answer_policy["deterministic_claims_forbidden_for"] = restricted
|
||||
answer_policy["blocked_techniques"] = blocked
|
||||
answer_policy["technique_truth_status"] = "restricted"
|
||||
evidence_status = _commercial_evidence_status()
|
||||
enriched["commercial_evidence_status"] = evidence_status
|
||||
consumer_context = enriched.get("consumer_context")
|
||||
if isinstance(consumer_context, dict):
|
||||
consumer_context["technique_truth"] = enriched["technique_truth"]
|
||||
consumer_context["commercial_evidence_status"] = evidence_status
|
||||
return enriched
|
||||
|
||||
|
||||
def _read_local_object(filename: str) -> dict[str, Any]:
|
||||
try:
|
||||
with (ORACLE_DIR / filename).open(encoding="utf-8") as handle:
|
||||
payload = json.load(handle)
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return {}
|
||||
return payload if isinstance(payload, dict) else {}
|
||||
|
||||
|
||||
def _commercial_evidence_status() -> dict[str, Any]:
|
||||
"""Summarize local evidence state without returning raw external responses."""
|
||||
vedastro = _read_local_object("vedastro_identity_archive_2026_07_19.json")
|
||||
mismatch = _read_local_object("three_engine_mismatch_arbitration_2026_07_19.json")
|
||||
return {
|
||||
"claim_audit": {
|
||||
"status": "contract_enforced",
|
||||
"scope": "commercial_claim_boundaries",
|
||||
},
|
||||
"vedastro_identity": {
|
||||
"status": vedastro.get("self_host_candidate_status") or "not_archived",
|
||||
"hosted_identity": "runtime_evidence_required",
|
||||
},
|
||||
"three_engine_mismatch": {
|
||||
"status": mismatch.get("status") or "not_assessed",
|
||||
"truth_policy": mismatch.get("truth_policy") or "no_majority_vote",
|
||||
"mismatch_count": mismatch.get("mismatch_count"),
|
||||
"category_counts": mismatch.get("category_counts") or {},
|
||||
},
|
||||
}
|
||||
@@ -40,8 +40,10 @@ if SCRIPTS_DIR not in sys.path:
|
||||
|
||||
try:
|
||||
from scripts.local_env import load_local_env
|
||||
from scripts.vedastro_runtime_context import temporary_timeout_seconds
|
||||
except ModuleNotFoundError: # pragma: no cover - script execution path
|
||||
from local_env import load_local_env
|
||||
from vedastro_runtime_context import temporary_timeout_seconds
|
||||
try:
|
||||
from scripts.unified_consultation_orchestrator import UnifiedConsultationOrchestrator
|
||||
except ModuleNotFoundError: # pragma: no cover - script execution path
|
||||
@@ -572,7 +574,8 @@ def execute_consultation_workflow(
|
||||
result['success'] = False
|
||||
result['blocked_reason'] = 'external_parity_not_passed'
|
||||
result['timing_precision_contract'] = build_timing_precision_contract(body.get('timing'))
|
||||
return result
|
||||
from scripts.commercial_skill_truth import apply_commercial_skill_truth
|
||||
return apply_commercial_skill_truth(result)
|
||||
|
||||
chart = dict(chart_override) if isinstance(chart_override, dict) else {}
|
||||
prashna = {}
|
||||
@@ -779,7 +782,8 @@ def execute_consultation_workflow(
|
||||
timing=body.get('timing'),
|
||||
reference_date=_consultation_reference_date(body).date().isoformat(),
|
||||
)
|
||||
return result
|
||||
from scripts.commercial_skill_truth import apply_commercial_skill_truth
|
||||
return apply_commercial_skill_truth(result)
|
||||
|
||||
|
||||
def _load_local_module(module_name):
|
||||
@@ -841,12 +845,16 @@ def _vedastro_runtime_fingerprint() -> dict:
|
||||
'endpoint_host': (urlparse(endpoint).netloc or '').lower(),
|
||||
'network_enabled': str(os.environ.get('VEDASTRO_ENABLE_NETWORK', '')).strip().lower() in {'1', 'true', 'yes'},
|
||||
'has_api_key': bool(os.environ.get('VEDASTRO_API_KEY', '').strip()),
|
||||
'timeout_seconds': str(os.environ.get('VEDASTRO_TIMEOUT_SECONDS', '')).strip(),
|
||||
'full_snapshot_fanout_enabled': str(
|
||||
os.environ.get('VEDASTRO_FULL_SNAPSHOT_FANOUT_ENABLED', '1')
|
||||
).strip().lower() in {'1', 'true', 'yes', 'on'},
|
||||
}
|
||||
|
||||
|
||||
def _build_api_chart_cache_payload(body: dict) -> dict:
|
||||
return {
|
||||
'cache_schema_version': 3,
|
||||
'cache_schema_version': 4,
|
||||
'birth': {
|
||||
'year': body.get('year'),
|
||||
'month': body.get('month'),
|
||||
@@ -960,6 +968,14 @@ def _async_job_ttl_seconds() -> float:
|
||||
return 3600.0
|
||||
|
||||
|
||||
def _async_high_rigor_vedastro_timeout_seconds() -> float:
|
||||
raw = str(os.environ.get('JYOTISH_ASYNC_HIGH_RIGOR_VEDASTRO_TIMEOUT_SECONDS', '90')).strip()
|
||||
try:
|
||||
return min(max(float(raw), 30.0), 180.0)
|
||||
except ValueError:
|
||||
return 90.0
|
||||
|
||||
|
||||
def _async_job_backend() -> str:
|
||||
return "sqlite" if os.environ.get("JYOTISH_ASYNC_JOB_BACKEND", "file").strip().lower() == "sqlite" else "file"
|
||||
|
||||
@@ -2759,7 +2775,8 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
|
||||
running['started_at'] = datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||
_write_high_rigor_job_record(job_id, running)
|
||||
try:
|
||||
result = self._compute_high_rigor_workflow_sync(body_copy)
|
||||
with temporary_timeout_seconds(_async_high_rigor_vedastro_timeout_seconds()):
|
||||
result = self._compute_high_rigor_workflow_sync(body_copy)
|
||||
completed = dict(running)
|
||||
completed['status'] = 'completed'
|
||||
completed['completed_at'] = datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||
@@ -6884,13 +6901,16 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
|
||||
|
||||
def _compute_active_rectification_events(self, body):
|
||||
allowed_fields = {
|
||||
'birth_date', 'start_time', 'end_time', 'lat', 'lon', 'tz', 'events',
|
||||
'birth_date', 'start_time', 'end_time', 'lat', 'lon', 'tz', 'events', 'high_rigor',
|
||||
}
|
||||
unsupported_fields = sorted(set(body) - allowed_fields)
|
||||
if unsupported_fields:
|
||||
raise BadRequest(
|
||||
f'unsupported active rectification event field: {unsupported_fields[0]}'
|
||||
)
|
||||
high_rigor = body.get('high_rigor', False)
|
||||
if not isinstance(high_rigor, bool):
|
||||
raise BadRequest('high_rigor must be a boolean')
|
||||
birth_date = body.get('birth_date')
|
||||
start_time = body.get('start_time')
|
||||
end_time = body.get('end_time')
|
||||
@@ -6911,7 +6931,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
|
||||
if not isinstance(events, list) or not 3 <= len(events) <= 6:
|
||||
raise BadRequest('events must contain between 3 and 6 items')
|
||||
normalized_events = []
|
||||
allowed_domains = {'education', 'relocation', 'relationship', 'career', 'health_pressure'}
|
||||
allowed_domains = {'education', 'relocation', 'relationship', 'career', 'finance', 'health_pressure'}
|
||||
formats = {'year': '%Y', 'month': '%Y-%m', 'day': '%Y-%m-%d'}
|
||||
for raw_event in events:
|
||||
if not isinstance(raw_event, dict) or set(raw_event) != {'id', 'domain', 'date', 'precision'}:
|
||||
@@ -6949,6 +6969,26 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
|
||||
'tz': tz,
|
||||
'events': normalized_events,
|
||||
})
|
||||
from scripts.rectification_technique_contract import build_rectification_technique_contract
|
||||
result['technique_contract'] = build_rectification_technique_contract(
|
||||
event_count=result.get('event_count', 0),
|
||||
domain_count=result.get('domain_count', 0),
|
||||
high_rigor=high_rigor,
|
||||
)
|
||||
if high_rigor:
|
||||
from scripts.rectification_three_engine_packet import build_packet
|
||||
result['three_engine_packet'] = build_packet({
|
||||
'year': parsed_birth_date.year,
|
||||
'month': parsed_birth_date.month,
|
||||
'day': parsed_birth_date.day,
|
||||
'hour': int(start_time.split(':', 1)[0]),
|
||||
'minute': int(start_time.split(':', 1)[1]),
|
||||
'lat': lat,
|
||||
'lon': lon,
|
||||
'tz': tz,
|
||||
})
|
||||
result['can_apply'] = False
|
||||
result.setdefault('reasons', []).append('three_engine_parity_not_passed')
|
||||
return {
|
||||
'success': True,
|
||||
'endpoint': 'active_rectification_events',
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
"""Commercial claim contract for birth-time rectification receipts."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
def build_rectification_technique_contract(*, event_count: int, domain_count: int, high_rigor: bool = False) -> dict[str, Any]:
|
||||
blockers: list[str] = []
|
||||
if event_count < 3:
|
||||
blockers.append("insufficient_events")
|
||||
if domain_count < 2:
|
||||
blockers.append("insufficient_domains")
|
||||
if high_rigor:
|
||||
blockers.append("three_engine_parity_not_passed")
|
||||
return {
|
||||
"schema_version": 1,
|
||||
"calculation_status": "not_started" if event_count == 0 else "evaluated",
|
||||
"used_divisional_charts": ["D4", "D9", "D10", "D24", "D30"],
|
||||
"used_arudha": ["A7", "UL", "A10"],
|
||||
"dasha_tracks": ["vimshottari_md_ad_pd", "narayana_md_ad"],
|
||||
"missing_layers": ["shadbala_kala_dig_chesta_total"],
|
||||
"partial_layers": ["D2", "D11", "shadbala_sthana_drik_naisargika"],
|
||||
"auxiliary_layers": ["functional_benefic_malefic", "controlled_transit", "ashtakavarga", "shadbala_verified_components"],
|
||||
"external_engines": {"status": "required_not_run" if high_rigor else "not_run", "providers": ["pyjhora", "jyotishganit", "vedastro"]},
|
||||
"hard_blockers": blockers,
|
||||
"can_narrow_to_minute": False,
|
||||
"boundary": "A candidate range is not a confirmed birth minute.",
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
"""Build a privacy-safe, request-level three-engine rectification parity packet."""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import importlib
|
||||
import json
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from domain_calculation_service import compute_chart
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
JYOTISHGANIT_ROOT = ROOT / "references" / "open_source_sources" / "jyotishganit"
|
||||
PLANETS = ("Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn")
|
||||
SIGNS = ("Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo", "Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces")
|
||||
|
||||
|
||||
def case_hash(case: dict[str, Any]) -> str:
|
||||
"""Stable identity for evidence correlation; never exposes birth data."""
|
||||
payload = json.dumps(case, sort_keys=True, ensure_ascii=True, separators=(",", ":"))
|
||||
return hashlib.sha256(payload.encode()).hexdigest()
|
||||
|
||||
|
||||
def _local_d1(case: dict[str, Any]) -> dict[str, str]:
|
||||
chart = compute_chart({**case, "ayanamsa": case.get("ayanamsa", "lahiri"), "node_mode": case.get("node_mode", "true")})
|
||||
return {planet: str(chart["planets"][planet]["sign"]) for planet in PLANETS}
|
||||
|
||||
|
||||
def _pyjhora_d1(case: dict[str, Any]) -> dict[str, str]:
|
||||
utils = importlib.import_module("jhora.utils")
|
||||
charts = importlib.import_module("jhora.horoscope.chart.charts")
|
||||
drik = importlib.import_module("jhora.panchanga.drik")
|
||||
jd = utils.julian_day_number((case["year"], case["month"], case["day"]), (case["hour"], case["minute"], case.get("second", 0)))
|
||||
drik.set_ayanamsa_mode("LAHIRI", jd=jd)
|
||||
place = drik.Place("request-level", case["lat"], case["lon"], case["tz"])
|
||||
index_to_planet = {0: "Sun", 1: "Moon", 2: "Mars", 3: "Mercury", 4: "Jupiter", 5: "Venus", 6: "Saturn"}
|
||||
return {index_to_planet[body]: SIGNS[int(position[0])] for body, position in charts.rasi_chart(jd, place) if body in index_to_planet}
|
||||
|
||||
|
||||
def _jyotishganit_d1(case: dict[str, Any]) -> dict[str, str]:
|
||||
sys.path.insert(0, str(JYOTISHGANIT_ROOT))
|
||||
try:
|
||||
from jyotishganit import calculate_birth_chart, get_birth_chart_json
|
||||
chart = calculate_birth_chart(datetime(case["year"], case["month"], case["day"], case["hour"], case["minute"], case.get("second", 0)), case["lat"], case["lon"], case["tz"], location_name="request-level", name="request-level")
|
||||
raw = get_birth_chart_json(chart)
|
||||
return {str(item["celestialBody"]): str(item["sign"]) for house in raw["d1Chart"]["houses"] for item in house.get("occupants", []) if item.get("celestialBody") in PLANETS}
|
||||
finally:
|
||||
if str(JYOTISHGANIT_ROOT) in sys.path:
|
||||
sys.path.remove(str(JYOTISHGANIT_ROOT))
|
||||
|
||||
|
||||
def build_packet(case: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Compare local/PyJHora/jyotishganit D1 without persisting private input."""
|
||||
required = {"year", "month", "day", "hour", "minute", "lat", "lon", "tz"}
|
||||
if not required <= set(case):
|
||||
raise ValueError("case is missing required birth fields")
|
||||
outputs: dict[str, dict[str, str]] = {"local": _local_d1(case)}
|
||||
engine_status: dict[str, str] = {"local": "ok"}
|
||||
for name, runner in (("pyjhora", _pyjhora_d1), ("jyotishganit", _jyotishganit_d1)):
|
||||
try:
|
||||
outputs[name] = runner(case)
|
||||
engine_status[name] = "ok"
|
||||
except Exception as exc:
|
||||
outputs[name] = {}
|
||||
engine_status[name] = f"blocked:{exc.__class__.__name__}"
|
||||
rows = [{"planet": planet, "values": {name: data.get(planet) for name, data in outputs.items()}, "status": "match" if len({data.get(planet) for data in outputs.values()}) == 1 else "mismatch"} for planet in PLANETS]
|
||||
return {
|
||||
"scope": "request_level_three_engine_d1_parity",
|
||||
"case_hash": case_hash(case),
|
||||
"engine_status": engine_status,
|
||||
"match_count": sum(row["status"] == "match" for row in rows),
|
||||
"mismatch_count": sum(row["status"] == "mismatch" for row in rows),
|
||||
"rows": rows,
|
||||
"vedastro": {"status": "requires_gateway_raw_archive"},
|
||||
"can_confirm": False,
|
||||
"boundary": "D1 parity alone never confirms a rectified minute; VedAstro raw and domain-level parity remain required.",
|
||||
}
|
||||
@@ -155,6 +155,7 @@ def _raw_response_archive(job_id: str, result: dict[str, Any]) -> dict[str, Any]
|
||||
archive_path = _queue_dir() / archive_rel
|
||||
archive_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
archive_path.write_text(json.dumps(raw, ensure_ascii=False, indent=2, sort_keys=True), encoding="utf-8")
|
||||
os.chmod(archive_path, 0o600)
|
||||
return {
|
||||
"status": "official_raw_response_archived",
|
||||
"official_raw_response_available": True,
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
"""Thread-local execution controls shared by every VedAstro import path."""
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import contextmanager
|
||||
import contextvars
|
||||
|
||||
|
||||
_TIMEOUT_OVERRIDE_SECONDS: contextvars.ContextVar[float | None] = contextvars.ContextVar(
|
||||
"vedastro_timeout_override_seconds",
|
||||
default=None,
|
||||
)
|
||||
|
||||
|
||||
def timeout_override_seconds() -> float | None:
|
||||
return _TIMEOUT_OVERRIDE_SECONDS.get()
|
||||
|
||||
|
||||
@contextmanager
|
||||
def temporary_timeout_seconds(seconds: float):
|
||||
token = _TIMEOUT_OVERRIDE_SECONDS.set(max(1.0, float(seconds)))
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
_TIMEOUT_OVERRIDE_SECONDS.reset(token)
|
||||
@@ -26,8 +26,10 @@ from urllib.parse import urlparse
|
||||
|
||||
try:
|
||||
from scripts.local_env import load_local_env
|
||||
from scripts.vedastro_runtime_context import timeout_override_seconds
|
||||
except ModuleNotFoundError: # pragma: no cover - script execution path
|
||||
from local_env import load_local_env
|
||||
from vedastro_runtime_context import timeout_override_seconds
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
@@ -357,6 +359,9 @@ _FREE_TIER_REQUEST_LOCK = threading.Lock()
|
||||
|
||||
|
||||
def _timeout_seconds() -> float:
|
||||
override = timeout_override_seconds()
|
||||
if override is not None:
|
||||
return override
|
||||
raw = os.environ.get(TIMEOUT_ENV, "").strip()
|
||||
if not raw:
|
||||
return DEFAULT_TIMEOUT_SECONDS
|
||||
@@ -366,6 +371,7 @@ def _timeout_seconds() -> float:
|
||||
return DEFAULT_TIMEOUT_SECONDS
|
||||
|
||||
|
||||
|
||||
def _backoff_seconds() -> float:
|
||||
raw = os.environ.get(BACKOFF_ENV, "").strip()
|
||||
if not raw:
|
||||
|
||||
Reference in New Issue
Block a user