feat(consult): rule engine always runs and merges with chart yogas (BUG-1174); closed-list traditional_yogas on general/career/wealth cards (BUG-1175)
Goldens regenerated by the capture scripts (PYTHONHASHSEED=0, cache TTL 0; two runs byte-identical). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
8e978cabc4
commit
c74bfcc06b
@@ -696,6 +696,121 @@ def build_yoga_domains(modules: dict) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def _yoga_key(entry: dict) -> str:
|
||||
name = "".join(ch for ch in str(entry.get("name") or "").lower() if ch.isalnum())
|
||||
return name.replace("yoga", "") or name
|
||||
|
||||
|
||||
def merge_detected_yogas(*groups: list) -> list[dict]:
|
||||
"""Merge yoga lists in order, dropping repeats by normalised name (BUG-1174)."""
|
||||
merged: list[dict] = []
|
||||
seen: set[str] = set()
|
||||
for group in groups:
|
||||
for entry in group or []:
|
||||
if not isinstance(entry, dict) or not entry.get("name"):
|
||||
continue
|
||||
key = _yoga_key(entry)
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
merged.append(entry)
|
||||
return merged
|
||||
|
||||
|
||||
def collect_consultation_yogas(handler, chart: dict, birth_payload: dict, planets: dict,
|
||||
ascendant: Any, compact_entry) -> list[dict]:
|
||||
"""Every yoga the consultation sieve sees: the chart's own list plus the rule engine.
|
||||
|
||||
BUG-1174 (TASK-consult-career-yoga-functional-20261002 T2): the rule engine
|
||||
used to run only when `chart.yogas` was empty, so a chart that already
|
||||
carried one or two yogas never had the Raman packet members checked. It now
|
||||
always runs; the two lists are merged and de-duplicated by name. No
|
||||
detection rule or sieve boundary changes.
|
||||
"""
|
||||
raw = chart.get("yogas") if isinstance(chart.get("yogas"), list) else []
|
||||
own = [entry for entry in (compact_entry(item) for item in raw) if entry]
|
||||
try:
|
||||
api = handler._compute_yogas_api({**birth_payload, "planets": planets, "ascendant": ascendant})
|
||||
except Exception:
|
||||
if not own:
|
||||
raise
|
||||
return merge_detected_yogas(own)
|
||||
result = api.get("result") if isinstance(api, dict) else {}
|
||||
result = result if isinstance(result, dict) else {}
|
||||
engine = [
|
||||
entry for entry in (
|
||||
compact_entry(item)
|
||||
for item in (result.get("extended_yogas") or []) + (result.get("rule_engine_yogas") or [])
|
||||
)
|
||||
if entry
|
||||
]
|
||||
return merge_detected_yogas(own, engine)
|
||||
|
||||
|
||||
# Closed list for `traditional_yogas` (TASK-consult-career-yoga-functional-20261002
|
||||
# D3, BUG-1175): canonical name -> name-token prefixes the detectors emit
|
||||
# (yoga_expansion / yoga_engine / chart.yogas / Raman packet hits). Nothing
|
||||
# outside this list is shown, and no detector is added here.
|
||||
TRADITIONAL_YOGA_LABEL = "传统格局,本站未验证,只作参考"
|
||||
TRADITIONAL_YOGAS = (
|
||||
("Ruchaka Yoga", ("ruchaka",)),
|
||||
("Bhadra Yoga", ("bhadra",)),
|
||||
("Hamsa Yoga", ("hamsa",)),
|
||||
("Malavya Yoga", ("malavya",)),
|
||||
("Sasa Yoga", ("sasa", "shasha")),
|
||||
("Gajakesari Yoga", ("gajakesari",)),
|
||||
("Neecha Bhanga Raja Yoga", ("neechabhanga", "neechbhanga")),
|
||||
# Budha-Aditya: the engine also emits the same Sun-Mercury conjunction as
|
||||
# "Surya-Budha Yoga"; both map to one row.
|
||||
("Budha-Aditya Yoga", ("budhaaditya", "budhaditya", "suryabudha")),
|
||||
)
|
||||
|
||||
|
||||
def _traditional_name(name: Any) -> str | None:
|
||||
token = "".join(ch for ch in str(name or "").lower() if ch.isalnum())
|
||||
for canonical, prefixes in TRADITIONAL_YOGAS:
|
||||
if any(token.startswith(prefix) for prefix in prefixes):
|
||||
return canonical
|
||||
return None
|
||||
|
||||
|
||||
def build_traditional_yogas(modules: dict) -> dict:
|
||||
"""Closed-list classical yogas the detectors found formed (BUG-1175).
|
||||
|
||||
Reads what the consultation sieve was handed (`raman_support.yogas`: packet
|
||||
hits plus `ungoverned_detected`). Only formed yogas are listed; the label is
|
||||
fixed. Reading rule (card): strength only, no stand-alone conclusion, never
|
||||
counted as affliction or support against affliction. Raman packets,
|
||||
`modules.yogas`, confidence caps and claim boundaries are unchanged.
|
||||
"""
|
||||
sieve = _record(_record(modules.get("raman_support")).get("yogas"))
|
||||
if sieve.get("status") != "executed":
|
||||
return _blocked("raman_support_yogas_missing")
|
||||
found: dict[str, list[str]] = {}
|
||||
for entry in list(sieve.get("hits") or []) + list(sieve.get("ungoverned_detected") or []):
|
||||
row = _record(entry)
|
||||
if row.get("hit") is False:
|
||||
continue
|
||||
canonical = _traditional_name(row.get("name"))
|
||||
if not canonical:
|
||||
continue
|
||||
planets = found.setdefault(canonical, [])
|
||||
for planet in row.get("planets") or []:
|
||||
if isinstance(planet, str) and planet not in planets:
|
||||
planets.append(planet)
|
||||
names = [
|
||||
f"{canonical}({'、'.join(found[canonical])})" if found[canonical] else canonical
|
||||
for canonical, _prefixes in TRADITIONAL_YOGAS
|
||||
if canonical in found
|
||||
]
|
||||
return {
|
||||
"status": "executed",
|
||||
"source": "raman_support.yogas (hits + ungoverned_detected), closed list TRADITIONAL_YOGAS",
|
||||
"label": TRADITIONAL_YOGA_LABEL,
|
||||
"names": names,
|
||||
}
|
||||
|
||||
|
||||
# ── Annual Tajika / Varshaphala ─────────────────────────────────────────────
|
||||
|
||||
def _annual_payload(birth_payload: dict, target_year: int) -> dict:
|
||||
@@ -889,6 +1004,7 @@ def attach_native_consultation_layers(chart: dict, *, birth_payload: dict, body:
|
||||
layers["inter_chart_linkage"] = _guard(build_inter_chart_linkage, engine, chart, modules)
|
||||
layers["graha_drishti"] = _guard(build_graha_drishti, engine, chart)
|
||||
layers["yoga_domains"] = _guard(build_yoga_domains, modules)
|
||||
layers["traditional_yogas"] = _guard(build_traditional_yogas, modules)
|
||||
if "annual" in routes:
|
||||
layers["annual_tajika"] = _guard(build_annual_tajika, birth_payload, chart, reference=reference)
|
||||
else:
|
||||
|
||||
@@ -1705,24 +1705,8 @@ def _attach_local_consultation_layers(handler, chart: dict, birth_payload: dict,
|
||||
|
||||
if not _consultation_layer_present(modules.get('yogas')):
|
||||
try:
|
||||
raw_yogas = chart.get('yogas') if isinstance(chart.get('yogas'), list) else []
|
||||
compact = [entry for entry in (_compact_yoga_entry(item) for item in raw_yogas) if entry]
|
||||
if not compact:
|
||||
api = handler._compute_yogas_api({
|
||||
**birth_payload,
|
||||
'planets': planets,
|
||||
'ascendant': ascendant,
|
||||
})
|
||||
result = api.get('result') if isinstance(api, dict) else {}
|
||||
extended = result.get('extended_yogas') if isinstance(result, dict) else []
|
||||
engine = result.get('rule_engine_yogas') if isinstance(result, dict) else []
|
||||
compact = [
|
||||
entry for entry in (
|
||||
_compact_yoga_entry(item)
|
||||
for item in (extended or []) + (engine or [])
|
||||
)
|
||||
if entry
|
||||
]
|
||||
from scripts.consultation_native_layers import collect_consultation_yogas # BUG-1174
|
||||
compact = collect_consultation_yogas(handler, chart, birth_payload, planets, ascendant, _compact_yoga_entry)
|
||||
routes = _consultation_declared_routes(body)
|
||||
raman = build_raman_support_observations(chart, compact, routes)
|
||||
sieved = raman['yogas']['yogas']
|
||||
|
||||
Reference in New Issue
Block a user