diff --git a/scripts/jyotish_engine.py b/scripts/jyotish_engine.py index 99943a89..8a350bd9 100644 --- a/scripts/jyotish_engine.py +++ b/scripts/jyotish_engine.py @@ -472,6 +472,29 @@ def calc_inter_chart_linkage(planet_name, d1_data, d9_data, d10_data, d12_data=N return result +def final_dispositor_from_chain(chain): + """Return a terminal only for a self-loop; retain multi-body cycles. + + Ported from upstream yinduzhanxing e9beae6b (TASK-upstream-sync4-20261002 T7). + """ + path = [] + for row in chain: + planet, target = row.get('planet'), row.get('dispositor') + if not planet or not target or (path and path[-1] != planet): + return None + if not path: + path.append(planet) + if target == planet: + return planet + if target in path: + cycle = path[path.index(target):] + rotations = [cycle[i:] + cycle[:i] for i in range(len(cycle))] + cycle = min(rotations) + return 'Cycle: ' + ' -> '.join(cycle + cycle[:1]) + path.append(target) + return None # incomplete or depth-limited chain is not a terminal + + def calc_all_dispositor_chains(planets_data): """为所有行星计算定位星链""" result = {} @@ -15023,21 +15046,20 @@ def _calc_dasa_convergence(dasha_result, chara_dasha_result, yogini_result, plan sc = info['system_count'] if sc >= 4: info['convergence_level'] = 'L5' - info['probability'] = '85-92%' info['interpretation'] = '四个及以上推运系统同时激活,顶级收敛信号' elif sc >= 3: info['convergence_level'] = 'L4' - info['probability'] = '75-85%' info['interpretation'] = '三系统同时激活,极强信号' elif sc >= 2: info['convergence_level'] = 'L3' - info['probability'] = '50-65%' info['interpretation'] = '双系统激活,强信号' else: info['convergence_level'] = 'L1' - info['probability'] = '+15-20%' info['interpretation'] = '单系统激活,需Transit确认' + info['probability'] = None + info['probability_basis'] = 'No empirical probability calibration; level counts activating systems only.' + # 收敛窗口(最高优先级的领域) top_domains = sorted(domain_activations.items(), key=lambda x: x[1]['system_count'], reverse=True)[:5] @@ -15725,23 +15747,8 @@ def cmd_full_reading(args): report['modules']['inter_chart_linkage'] = ic_result # 补充:每个行星的"最终定位星"(Dispositor Chain 的最后一个非循环元素) - final_dispositors = {} - for pname, chain in dc_result.items(): - if chain: - # 找到最后一个不重复的元素 - seen = set() - last_valid = None - for item in chain: - if item['dispositor'] in seen: - break - seen.add(item['dispositor']) - last_valid = item - if last_valid: - final_dispositors[pname] = last_valid['dispositor'] - else: - final_dispositors[pname] = chain[-1]['dispositor'] if chain else None - else: - final_dispositors[pname] = None + # A multi-planet cycle has no unique terminal dispositor. + final_dispositors = {p: final_dispositor_from_chain(c) for p, c in dc_result.items()} report['modules']['final_dispositors'] = final_dispositors except Exception as e: diff --git a/tests/test_dispositor_cycle_resolution.py b/tests/test_dispositor_cycle_resolution.py new file mode 100644 index 00000000..5855ba3a --- /dev/null +++ b/tests/test_dispositor_cycle_resolution.py @@ -0,0 +1,38 @@ +"""Do not collapse a directed dispositor cycle into an arbitrary terminal.""" +import ast +from pathlib import Path + + +def resolver(): + # Isolate the pure function without importing the large CLI and adapters. + source = Path(__file__).parents[1] / 'scripts/jyotish_engine.py' + tree = ast.parse(source.read_text()) + node = next(n for n in tree.body if isinstance(n, ast.FunctionDef) + and n.name == 'final_dispositor_from_chain') + namespace = {} + exec(compile(ast.Module(body=[node], type_ignores=[]), str(source), 'exec'), namespace) + return namespace[node.name] + + +def chain(*names): + return [{'planet':a,'dispositor':b} for a,b in zip(names,names[1:])] + + +def test_cycle_rotation_and_leading_planet(): + f = resolver() + expected = 'Cycle: Jupiter -> Mercury -> Jupiter' + assert f(chain('Mercury','Jupiter','Mercury')) == expected + assert f(chain('Jupiter','Mercury','Jupiter')) == expected + assert f(chain('Venus','Jupiter','Mercury','Jupiter')) == expected + + +def test_terminal_and_incomplete_are_distinct(): + f = resolver() + assert f(chain('Sun','Mars','Saturn','Saturn')) == 'Saturn' + assert f(chain('Sun','Mars')) is None + assert f([]) is None + assert f(chain('Sun','Mars') + chain('Venus','Venus')) is None + + +def test_three_body_cycle_preserves_direction(): + assert resolver()(chain('Sun','Mars','Moon','Sun')) == 'Cycle: Mars -> Moon -> Sun -> Mars'