Files
Jyotisha/scripts/book_rule_layer.py
T
ClaudeandClaude Opus 5.5 8ed5b5f3c9 feat(engine): port upstream skill calculation fixes into the engine (BUG-1699)
- affliction_evidence / career_semantic_profile / career_analysis (functional
  roles, affliction signals, user-industry gate)
- sade_sati.build_saturn_cycle_timeline with bisected ingress moments
  (FLG_SIDEREAL frame, BUG-1275)
- Chesta profile candidates and PL9 AV cell profile as sidecars
  (shadbala.py / ashtakavarga.py stay frozen, ERR-110)
- Raman Sahams in the annual pack; book rule packets + admission gate
- consultation_native_layers: affliction_evidence, career_analysis,
  sade_sati_timeline, shadbala_chesta_profiles, book_rules,
  annual_tajika.raman_sahams

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DbBXoyn7Hj3LCW3BUs5bZ8
2026-11-07 14:42:54 +08:00

136 lines
5.9 KiB
Python

#!/usr/bin/env python3
"""Book / web rule layer for the consultation output (BUG-1699).
Upstream yinduzhanxing keeps copyright-safe rule summaries in
``references/book_rule_packets.json`` (plus the web intakes
``web_rule_intake_2026_10_06.json`` and ``..._romance.json``, merged into the
packets) and gates them with ``scripts/book_admission_gate.build_report``:
only a ``validated_reference`` packet with a formula-grade receipt becomes a
runtime packet, and admission never implies Technique Audit execution.
This module reads the same files with the same gate and hands the
consultation one object: the runtime packets (rules a reading may cite as a
validated method), and the route's ``reference_only`` / ``candidate_mapped``
rules, each with its summary, boundary and whether this repository carries
an implementation. It computes nothing about the chart.
"""
from __future__ import annotations
import json
from functools import lru_cache
from pathlib import Path
from typing import Any
ROOT = Path(__file__).resolve().parents[1]
PACKETS_PATH = ROOT / "references" / "book_rule_packets.json"
INTAKE_PATHS = (
ROOT / "references" / "web_rule_intake_2026_10_06.json",
ROOT / "references" / "web_rule_intake_2026_10_06_romance.json",
)
SCHEMA = "jyotish.book_rule_layer.v1"
# Rule ids are matched by these words to a consultation route. A rule with no
# match is listed under no route (it still counts in admission_counts).
ROUTE_KEYWORDS = {
"marriage": ("marriage", "spouse", "upapada", "meeting", "seventh", "darakaraka"),
"timing": ("dasha", "transit", "timing"),
"annual": ("annual", "pravesha", "varsha", "karana", "tithi", "sudarsana", "saptarishi"),
"career": ("career", "karma", "tenth"),
"wealth": ("wealth", "hora_lagna", "dhana"),
"general": ("interpretation", "strength", "arudha", "argala", "varga"),
}
CLAIM_BOUNDARY = (
"Admission state never implies Technique Audit execution. Only runtime_packets may be cited as a "
"validated method; reference_only and candidate_mapped rules may frame a question or a check, never "
"ground a conclusion or a date. Romance intake rules describe where/how a meeting may happen and must "
"not be used to date it."
)
def _load(path: Path) -> dict[str, Any]:
try:
data = json.loads(path.read_text(encoding="utf-8"))
except (OSError, ValueError):
return {}
return data if isinstance(data, dict) else {}
@lru_cache(maxsize=1)
def _registry() -> dict[str, Any]:
try:
from book_admission_gate import build_report
except ImportError: # pragma: no cover - package import path
from scripts.book_admission_gate import build_report
packets_doc = _load(PACKETS_PATH)
packets = [row for row in packets_doc.get("packets", []) if isinstance(row, dict) and row.get("rule_id")]
gate = build_report({"cards": []}, {"packets": packets})
intake_ids: dict[str, str] = {}
for path in INTAKE_PATHS:
for row in _load(path).get("packets", []):
if isinstance(row, dict) and row.get("rule_id"):
intake_ids[str(row["rule_id"])] = path.name
return {"packets": packets, "gate": gate, "intake_ids": intake_ids,
"copyright_boundary": packets_doc.get("copyright_boundary")}
def _implemented_here(packet: dict[str, Any]) -> bool | None:
targets = [t for t in packet.get("implementation_targets") or [] if isinstance(t, str) and t.startswith("scripts/")]
if not targets:
return None
return all((ROOT / target).is_file() for target in targets)
def _row(packet: dict[str, Any], runtime_ids: set[str], intake_ids: dict[str, str]) -> dict[str, Any]:
validation = packet.get("validation") if isinstance(packet.get("validation"), dict) else {}
return {
"rule_id": packet["rule_id"],
"source_id": packet.get("source_id"),
"admission_state": packet.get("admission_state"),
"runtime_usable": packet["rule_id"] in runtime_ids,
"summary": packet.get("non_verbatim_rule_summary"),
"assertion_boundary": packet.get("assertion_boundary"),
"page_locator": packet.get("page_locator"),
"validation_status": validation.get("status"),
"implemented_here": _implemented_here(packet),
"intake_file": intake_ids.get(packet["rule_id"]),
}
def route_rule_ids(rule_id: str) -> list[str]:
lowered = rule_id.lower()
return [route for route, words in ROUTE_KEYWORDS.items() if any(word in lowered for word in words)]
def build_book_rule_layer(routes: set[str] | list[str] | None = None) -> dict[str, Any]:
registry = _registry()
packets = registry["packets"]
if not packets:
return {"schema": SCHEMA, "status": "blocked", "reason": "book_rule_packets_missing"}
gate = registry["gate"]
runtime_ids = set(gate.get("runtime_packet_ids") or [])
intake_ids = registry["intake_ids"]
wanted = {str(route).lower() for route in (routes or [])}
counts: dict[str, int] = {}
for packet in packets:
state = str(packet.get("admission_state"))
counts[state] = counts.get(state, 0) + 1
runtime = [_row(p, runtime_ids, intake_ids) for p in packets if p["rule_id"] in runtime_ids]
route_rules = [
_row(p, runtime_ids, intake_ids)
for p in packets
if p["rule_id"] not in runtime_ids and wanted.intersection(route_rule_ids(p["rule_id"]))
]
return {
"schema": SCHEMA,
"status": "executed",
"source": "references/book_rule_packets.json + scripts/book_admission_gate.build_report",
"routes": sorted(wanted),
"admission_counts": dict(sorted(counts.items())),
"runtime_packets": runtime,
"route_reference_rules": route_rules,
"gate_failures": gate.get("failures") or [],
"intake_files": [path.name for path in INTAKE_PATHS if path.is_file()],
"copyright_boundary": registry.get("copyright_boundary"),
"claim_boundary": CLAIM_BOUNDARY,
}