From 9e46a4814a02bc6c3eaf370aef0a43dad2c0a89f Mon Sep 17 00:00:00 2001 From: 732642856 <732642856@qq.com> Date: Wed, 8 Jul 2026 17:16:39 +0800 Subject: [PATCH] feat: add active rectification questionnaire --- .../birth-time-rectification-decision-tree.md | 28 ++++++ scripts/active_rectification_questions.py | 99 +++++++++++++++++++ tests/test_active_rectification_questions.py | 18 ++++ tests/test_rectification_decision_tree_doc.py | 2 + 4 files changed, 147 insertions(+) create mode 100644 scripts/active_rectification_questions.py create mode 100644 tests/test_active_rectification_questions.py diff --git a/references/birth-time-rectification-decision-tree.md b/references/birth-time-rectification-decision-tree.md index 4b3bc207..549b42c1 100644 --- a/references/birth-time-rectification-decision-tree.md +++ b/references/birth-time-rectification-decision-tree.md @@ -31,6 +31,34 @@ - [jyotish-app/rectification-engine.js](/jyotish-app/rectification-engine.js) - [references/varga-system-quick-reference.md](/references/varga-system-quick-reference.md) +## 1.1 产品化问询原则 + +生时校正不应默认要求用户先列出 8-15 个事件。普通用户路径优先采用主动问询: + +```text +候选时间扫描 +→ 分盘 / Arudha / KP cusp / Dasha 敏感差异提取 +→ 生成高信息量选择题 +→ 用户回答 A/B/C/D +→ 动态缩小候选时间簇 +→ 进入下一轮追问 +``` + +用户只需回答: + +```text +A. 明确有,且时间大致吻合 +B. 有类似,但时间略偏或不够重大 +C. 没有明显发生 +D. 不确定 / 不记得 +``` + +机器可审计入口: + +```bash +python3 scripts/active_rectification_questions.py --birth-time "1955-02-24 19:15" --uncertainty-minutes 30 --pretty +``` + ## 2. 第一层:主引擎 ### Dasha + dated events diff --git a/scripts/active_rectification_questions.py b/scripts/active_rectification_questions.py new file mode 100644 index 00000000..4c7bdc46 --- /dev/null +++ b/scripts/active_rectification_questions.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +"""Generate active-choice birth-time rectification questions.""" + +from __future__ import annotations + +import argparse +import json +from datetime import datetime, timedelta +from typing import Any + +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: + return datetime.strptime(value, "%Y-%m-%d %H:%M") + + +def _candidate_scan(center: datetime, uncertainty_minutes: int, step_minutes: int) -> dict[str, Any]: + start = center - timedelta(minutes=uncertainty_minutes) + end = center + timedelta(minutes=uncertainty_minutes) + return { + "start": start.strftime("%Y-%m-%d %H:%M"), + "end": end.strftime("%Y-%m-%d %H:%M"), + "step_minutes": step_minutes, + "candidate_count": int((end - start).total_seconds() // 60 // step_minutes) + 1, + "cluster_labels": ["early_candidate_cluster", "middle_candidate_cluster", "late_candidate_cluster"], + } + + +def build_questionnaire(birth_time: str, uncertainty_minutes: int = 30, step_minutes: int = 1) -> dict[str, Any]: + 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, + "candidate_scan": _candidate_scan(_parse_time(birth_time), uncertainty_minutes, step_minutes), + "workflow": [ + "candidate_time_scan", + "varga_arudha_kp_sensitivity_diff", + "high_information_question_generation", + "multiple_choice_user_answers", + "dynamic_candidate_cluster_scoring", + "next_round_question_selection", + ], + "rounds": { + "1": "coarse screen", + "2": "domain follow-up", + "3": "fine confirmation", + }, + "sensitivity_layers": ["D9", "D10", "D24", "D30", "D60", "D4", "UL", "A7", "A10", "KP_cusp", "Vimshottari", "Narayana", "Chara"], + "questions": questions, + "boundary": "Question generation only; final rectification requires scoring answers against actual candidate chart differences.", + } + + +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") + parser.add_argument("--uncertainty-minutes", type=int, default=30) + parser.add_argument("--step-minutes", type=int, default=1) + parser.add_argument("--pretty", action="store_true") + args = parser.parse_args() + print(json.dumps(build_questionnaire(args.birth_time, args.uncertainty_minutes, args.step_minutes), ensure_ascii=False, indent=2 if args.pretty else None)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_active_rectification_questions.py b/tests/test_active_rectification_questions.py new file mode 100644 index 00000000..36527e09 --- /dev/null +++ b/tests/test_active_rectification_questions.py @@ -0,0 +1,18 @@ +from __future__ import annotations + +from scripts.active_rectification_questions import build_questionnaire + + +def test_active_rectification_questions_generate_choice_based_workflow() -> None: + report = build_questionnaire("1955-02-24 19:15", uncertainty_minutes=30) + + assert report["scope"] == "active_birth_time_rectification_questionnaire" + assert report["candidate_scan"]["start"] == "1955-02-24 18:45" + assert report["candidate_scan"]["end"] == "1955-02-24 19:45" + assert report["candidate_scan"]["candidate_count"] == 61 + assert "high_information_question_generation" in report["workflow"] + assert {"D9", "D10", "D24", "D30", "UL", "A10", "KP_cusp"} <= set(report["sensitivity_layers"]) + assert len(report["questions"]) >= 8 + assert {q["round"] for q in report["questions"]} == {1, 2, 3} + assert all({option["key"] for option in q["options"]} == {"A", "B", "C", "D"} for q in report["questions"]) + assert all("scoring_map" in q for q in report["questions"]) diff --git a/tests/test_rectification_decision_tree_doc.py b/tests/test_rectification_decision_tree_doc.py index 3ad9bd6e..cd06dc06 100644 --- a/tests/test_rectification_decision_tree_doc.py +++ b/tests/test_rectification_decision_tree_doc.py @@ -21,6 +21,8 @@ def test_rectification_decision_tree_doc_exists_and_covers_priority_layers() -> assert "D60" in text assert "不是所有分盘一股脑上" in text assert "Dasha 定框,D9/D10 定核心" in text + assert "主动问询" in text + assert "scripts/active_rectification_questions.py" in text def test_rectification_decision_tree_doc_maps_event_groups_to_vargas() -> None: