Spaces:
Running
Running
Download rule_contract.py from sisithefox/astrologybazi-api: direct link, hf CLI and curl.
- Browser
- Download file 14.1 kB
-
https://huggingface.co/spaces/sisithefox/astrologybazi-api/resolve/main/rule_contract.py
- Command line
-
hf download hf://spaces/sisithefox/astrologybazi-api/rule_contract.py
-
curl -L -o rule_contract.py https://huggingface.co/spaces/sisithefox/astrologybazi-api/resolve/main/rule_contract.py
14.1 kB
| """Auditable registry for the current Da Yun scoring signals. | |
| This module is intentionally *not* imported by the production calculation | |
| path. It describes what the existing heuristic scorer does, where each signal | |
| is implemented, and what evidence it does (and does not) have. Keeping this | |
| registry separate prevents a traditional rule, a library default, and an | |
| empirical claim from being silently treated as the same kind of authority. | |
| The registry is a guardrail for future changes, not a claim that the listed | |
| rules predict life outcomes. Every weighted signal remains uncalibrated until | |
| we have a blinded, labelled evaluation set. | |
| """ | |
| from __future__ import annotations | |
| import re | |
| from typing import Any, Dict, Iterable, List, Mapping, Optional, Sequence | |
| RULE_CONTRACT_VERSION = "dayun-rules-v1" | |
| SCORE_METHOD = "heuristic-v2-single-relation-source" | |
| SCORE_CONFIDENCE = "low" | |
| # A time split is a traditional attribution convention, not a measured | |
| # probability of an event in one half of a decade. | |
| TIME_ATTRIBUTION_POLICY: Dict[str, Any] = { | |
| "stem": { | |
| "window": "first_5_years", | |
| "status": "heuristic", | |
| "implementation": "dayun_analyzer.py:_judge_dayun_quality", | |
| }, | |
| "branch": { | |
| "window": "last_5_years", | |
| "status": "heuristic", | |
| "implementation": "dayun_analyzer.py:_judge_dayun_quality", | |
| }, | |
| "sui_yun_bing_lin": { | |
| "window": "matching_liunian_only", | |
| "status": "event_marker_not_decade_weight", | |
| "implementation": "dayun_analyzer.py:_find_sui_yun_bing_lin_years", | |
| }, | |
| } | |
| def _rule( | |
| rule_id: str, | |
| category: str, | |
| description: str, | |
| effect: str, | |
| weight: Any, | |
| implementation: str, | |
| double_count_group: str, | |
| *, | |
| unit: Optional[str] = None, | |
| evidence_status: str = "unvalidated", | |
| source_kind: str = "traditional-heuristic", | |
| source_refs: Sequence[str] = (), | |
| notes: str = "", | |
| ) -> Dict[str, Any]: | |
| """Create one normalized rule entry. | |
| An empty ``source_refs`` list is allowed when the only provenance is the | |
| current code or an unverified traditional convention. The validator then | |
| keeps the entry in the unvalidated bucket. | |
| """ | |
| return { | |
| "id": rule_id, | |
| "category": category, | |
| "description": description, | |
| "effect": effect, | |
| "weight": weight, | |
| "unit": unit or ("score_points" if effect in {"positive", "negative", "mixed"} else "policy"), | |
| "implementation": implementation, | |
| "double_count_group": double_count_group, | |
| "evidence_status": evidence_status, | |
| "source_kind": source_kind, | |
| "source_refs": list(source_refs), | |
| "notes": notes, | |
| } | |
| # This is a map of the current scorer, not a proposed list of new rules. The | |
| # explicit groups make it possible to reject a future second 六合/冲/害 | |
| # implementation before it changes a user's score. | |
| RULE_SPECS: Sequence[Mapping[str, Any]] = ( | |
| _rule( | |
| "dayun.base.score", "baseline", "Start each decade at the neutral baseline", | |
| "neutral", 50, "dayun_analyzer.py:_judge_dayun_quality", "baseline", | |
| unit="score_points", | |
| notes="Display score only; not a percentile or probability.", | |
| ), | |
| _rule( | |
| "dayun.stem.yongshen_element", "element_alignment", "Stem matches Yongshen element", | |
| "positive", 18, "dayun_analyzer.py:_judge_dayun_quality", "stem_element", | |
| notes="The five-year attribution is a traditional convention.", | |
| ), | |
| _rule( | |
| "dayun.stem.jishen_element", "element_alignment", "Stem matches Jishen element", | |
| "negative", -15, "dayun_analyzer.py:_judge_dayun_quality", "stem_element", | |
| ), | |
| _rule( | |
| "dayun.branch.yongshen_element", "element_alignment", "Branch matches Yongshen element", | |
| "positive", 22, "dayun_analyzer.py:_judge_dayun_quality", "branch_element", | |
| notes="The five-year attribution is a traditional convention.", | |
| ), | |
| _rule( | |
| "dayun.branch.jishen_element", "element_alignment", "Branch matches Jishen element", | |
| "negative", -18, "dayun_analyzer.py:_judge_dayun_quality", "branch_element", | |
| ), | |
| _rule( | |
| "dayun.siku.muku.weak", "siku", "Weak day master meets assigned tomb/storehouse", | |
| "negative", -18, "dayun_analyzer.py:_judge_dayun_quality", "siku_role", | |
| notes="Traditional structural signal; no outcome calibration yet.", | |
| ), | |
| _rule( | |
| "dayun.siku.muku.strong", "siku", "Strong day master meets assigned tomb/storehouse", | |
| "positive", 10, "dayun_analyzer.py:_judge_dayun_quality", "siku_role", | |
| ), | |
| _rule( | |
| "dayun.siku.caiku.weak", "siku", "Weak day master meets assigned wealth storehouse", | |
| "negative", -12, "dayun_analyzer.py:_judge_dayun_quality", "siku_role", | |
| ), | |
| _rule( | |
| "dayun.siku.caiku.strong", "siku", "Strong day master meets assigned wealth storehouse", | |
| "positive", 20, "dayun_analyzer.py:_judge_dayun_quality", "siku_role", | |
| ), | |
| _rule( | |
| "dayun.siku.storage_yongshen", "siku", "Storehouse hidden element matches Yongshen", | |
| "positive", 15, "dayun_analyzer.py:_judge_dayun_quality", "siku_role", | |
| ), | |
| _rule( | |
| "dayun.siku.storage_jishen", "siku", "Storehouse hidden element matches Jishen", | |
| "negative", -12, "dayun_analyzer.py:_judge_dayun_quality", "siku_role", | |
| ), | |
| _rule( | |
| "dayun.siku.hidden_yongshen", "siku", "Neutral storehouse contains residual Yongshen qi", | |
| "positive", 5, "dayun_analyzer.py:_judge_dayun_quality", "siku_role", | |
| ), | |
| _rule( | |
| "dayun.combination.both_yongshen", "combination", "Stem and branch both match Yongshen", | |
| "positive", 15, "dayun_analyzer.py:_judge_dayun_quality", "element_combination", | |
| ), | |
| _rule( | |
| "dayun.combination.both_jishen", "combination", "Stem and branch both match Jishen", | |
| "negative", -5, "dayun_analyzer.py:_judge_dayun_quality", "element_combination", | |
| ), | |
| _rule( | |
| "dayun.same_day_master_stem", "stem_relation", "Dayun stem equals day-master stem", | |
| "positive", 8, "dayun_analyzer.py:_judge_dayun_quality", "stem_relation", | |
| notes="Suppressed for the weak-storehouse risk guardrail.", | |
| ), | |
| _rule( | |
| "dayun.tian_ke_di_chong", "pillar_relation", "Dayun forms Tian Ke Di Chong with day pillar", | |
| "negative", -25, "dayun_analyzer.py:_judge_dayun_quality", "pillar_relation", | |
| ), | |
| _rule( | |
| "dayun.fu_yin", "pillar_relation", "Dayun pillar repeats a natal pillar", | |
| "negative", -15, "dayun_analyzer.py:_judge_dayun_quality", "pillar_relation", | |
| ), | |
| _rule( | |
| "dayun.shensha.tianyi", "shensha", "Tianyi Guiren marker", | |
| "positive", 15, "dayun_analyzer.py:_judge_dayun_quality", "shensha", | |
| notes="Auxiliary symbolic marker; never sufficient on its own.", | |
| ), | |
| _rule( | |
| "dayun.shensha.yima", "shensha", "Yima marker", | |
| "positive", 8, "dayun_analyzer.py:_judge_dayun_quality", "shensha", | |
| notes="Auxiliary symbolic marker; effect is not an outcome probability.", | |
| ), | |
| _rule( | |
| "dayun.shensha.taohua", "shensha", "Taohua/Xianchi marker", | |
| "positive", 5, "dayun_analyzer.py:_judge_dayun_quality", "shensha", | |
| notes="Auxiliary marker; relationship outcomes require reality checks.", | |
| ), | |
| _rule( | |
| "dayun.shensha.yangren", "shensha", "Yangren marker", | |
| "negative", -10, "dayun_analyzer.py:_judge_dayun_quality", "shensha", | |
| notes="Auxiliary marker; health language must stay non-diagnostic.", | |
| ), | |
| _rule( | |
| "dayun.shensha.kongwang", "shensha", "Kongwang marker", | |
| "negative", -12, "dayun_analyzer.py:_judge_dayun_quality", "shensha", | |
| notes="Auxiliary marker; not independently validated.", | |
| ), | |
| _rule( | |
| "dayun.tengod.bijie", "ten_god", "Fellow/rob-wealth ten-god is favorable", | |
| "positive", 12, "dayun_analyzer.py:_judge_dayun_quality", "ten_god", | |
| ), | |
| _rule( | |
| "dayun.tengod.wealth", "ten_god", "Wealth ten-god is favorable", | |
| "positive", 15, "dayun_analyzer.py:_judge_dayun_quality", "ten_god", | |
| ), | |
| _rule( | |
| "dayun.tengod.official", "ten_god", "Officer/Seven-killings ten-god is favorable", | |
| "positive", 15, "dayun_analyzer.py:_judge_dayun_quality", "ten_god", | |
| ), | |
| _rule( | |
| "dayun.tengod.resource", "ten_god", "Resource ten-god is favorable", | |
| "positive", 15, "dayun_analyzer.py:_judge_dayun_quality", "ten_god", | |
| ), | |
| _rule( | |
| "dayun.relationship.liuhe", "relationship", "Liuhe adjustment from single calculator", | |
| "positive", 10, "xingchonghehe_calculator.py:check_dayun_relationship", "relationship_calculator", | |
| notes="Only this calculator may apply Liuhe/Chong/Hai/Xing/Sanhe weights.", | |
| ), | |
| _rule( | |
| "dayun.relationship.risky_he", "relationship", "Risky Liuhe adjustment from single calculator", | |
| "positive", 5, "xingchonghehe_calculator.py:check_dayun_relationship", "relationship_calculator", | |
| ), | |
| _rule( | |
| "dayun.relationship.chong", "relationship", "Liu Chong adjustment by severity", | |
| "negative", "-severity*5", "xingchonghehe_calculator.py:check_dayun_relationship", "relationship_calculator", | |
| ), | |
| _rule( | |
| "dayun.relationship.hai", "relationship", "Liu Hai adjustment by severity", | |
| "negative", "-severity*2", "xingchonghehe_calculator.py:check_dayun_relationship", "relationship_calculator", | |
| ), | |
| _rule( | |
| "dayun.relationship.xing", "relationship", "New Xing adjustment by severity", | |
| "negative", "-severity*5", "xingchonghehe_calculator.py:check_dayun_relationship", "relationship_calculator", | |
| ), | |
| _rule( | |
| "dayun.relationship.sanhe_half", "relationship", "New half-Sanhe adjustment before sign", | |
| "mixed", 12, "xingchonghehe_calculator.py:check_dayun_relationship", "relationship_calculator", | |
| ), | |
| _rule( | |
| "dayun.relationship.sanhe_completion", "relationship", "Half-Sanhe to completed-Sanhe adjustment", | |
| "mixed", 8, "xingchonghehe_calculator.py:check_dayun_relationship", "relationship_calculator", | |
| ), | |
| _rule( | |
| "dayun.relationship.sanhe_full", "relationship", "New completed-Sanhe adjustment before sign", | |
| "mixed", 20, "xingchonghehe_calculator.py:check_dayun_relationship", "relationship_calculator", | |
| ), | |
| _rule( | |
| "dayun.score.clamp", "output_guardrail", "Clamp heuristic score to display range 0..100", | |
| "neutral", "0..100", "dayun_analyzer.py:_judge_dayun_quality", "output_guardrail", | |
| notes="Display bound, not probability calibration.", | |
| ), | |
| ) | |
| RULES_BY_ID: Dict[str, Mapping[str, Any]] = { | |
| str(rule["id"]): rule for rule in RULE_SPECS | |
| } | |
| _RULE_ID_PATTERN = re.compile(r"^[a-z0-9]+(?:[._-][a-z0-9]+)+$") | |
| _EVIDENCE_STATUSES = { | |
| "unvalidated", "calendar-verified", "shadow-checked", "empirically-calibrated", | |
| } | |
| _SOURCE_KINDS = { | |
| "traditional-heuristic", "internal-code", "external-calendar", "shadow-implementation", | |
| } | |
| def weighted_rule_ids() -> List[str]: | |
| """Return rules which can change a score, excluding policy-only entries.""" | |
| return [str(rule["id"]) for rule in RULE_SPECS if rule["unit"] == "score_points"] | |
| def validate_rule_contract(rules: Iterable[Mapping[str, Any]] = RULE_SPECS) -> Dict[str, Any]: | |
| """Validate registry invariants and return a machine-readable report.""" | |
| entries = list(rules) | |
| errors: List[str] = [] | |
| seen: set[str] = set() | |
| for index, rule in enumerate(entries): | |
| prefix = f"rules[{index}]" | |
| rule_id = str(rule.get("id", "")) | |
| if not rule_id or not _RULE_ID_PATTERN.fullmatch(rule_id): | |
| errors.append(f"{prefix}.id is missing or malformed: {rule_id!r}") | |
| if rule_id in seen: | |
| errors.append(f"duplicate rule id: {rule_id}") | |
| seen.add(rule_id) | |
| for key in ("category", "description", "effect", "implementation", "double_count_group"): | |
| if not rule.get(key): | |
| errors.append(f"{prefix}.{key} is required") | |
| if rule.get("effect") not in {"neutral", "positive", "negative", "mixed"}: | |
| errors.append(f"{prefix}.effect is invalid: {rule.get('effect')!r}") | |
| if rule.get("evidence_status") not in _EVIDENCE_STATUSES: | |
| errors.append(f"{prefix}.evidence_status is invalid: {rule.get('evidence_status')!r}") | |
| if rule.get("source_kind") not in _SOURCE_KINDS: | |
| errors.append(f"{prefix}.source_kind is invalid: {rule.get('source_kind')!r}") | |
| if rule.get("unit") == "score_points" and rule.get("weight") is None: | |
| errors.append(f"{prefix}.weighted rule has no weight") | |
| if rule.get("evidence_status") == "empirically-calibrated": | |
| errors.append(f"{prefix} cannot claim empirical calibration before a labelled protocol") | |
| group_to_ids: Dict[str, List[str]] = {} | |
| for rule in entries: | |
| group = str(rule.get("double_count_group", "")) | |
| group_to_ids.setdefault(group, []).append(str(rule.get("id", ""))) | |
| weighted_unvalidated = [ | |
| str(rule["id"]) | |
| for rule in entries | |
| if rule.get("unit") == "score_points" | |
| and rule.get("evidence_status") != "empirically-calibrated" | |
| ] | |
| return { | |
| "version": RULE_CONTRACT_VERSION, | |
| "score_method": SCORE_METHOD, | |
| "score_confidence": SCORE_CONFIDENCE, | |
| "passed": not errors, | |
| "rule_count": len(entries), | |
| "weighted_rule_count": len(weighted_unvalidated), | |
| "weighted_unvalidated": weighted_unvalidated, | |
| "double_count_groups": group_to_ids, | |
| "time_attribution_policy": TIME_ATTRIBUTION_POLICY, | |
| "errors": errors, | |
| } | |
| def get_rule(rule_id: str) -> Mapping[str, Any]: | |
| """Return one rule or raise a useful error for an unregistered rule.""" | |
| try: | |
| return RULES_BY_ID[rule_id] | |
| except KeyError as exc: | |
| raise KeyError(f"Unregistered Dayun rule: {rule_id}") from exc | |
| if __name__ == "__main__": | |
| import json | |
| print(json.dumps(validate_rule_contract(), ensure_ascii=False, indent=2)) | |