research: consultation evidence-card inventory and draft
Measure the model-visible consultation payload on three public charts, draft per-domain cards, and record the Narayana/pratyantar projection gap as BUG-1054. No runtime behavior change.
This commit is contained in:
@@ -0,0 +1,811 @@
|
||||
"""Pure helpers for the consultation evidence-card research.
|
||||
|
||||
No network, no engine mutation. Classification partitions a JSON value so the
|
||||
five category sizes sum to the canonical serialization length.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
CATEGORIES = ("core", "status", "not_applicable", "research", "western")
|
||||
|
||||
TOKEN_CHARS_PER_TOKEN = 3.5
|
||||
TOKEN_METHOD = (
|
||||
"chars/3.5. The 2026-09-27 diagnosis measured about 144000 projected "
|
||||
"characters as about 40000 tokens (3.6 chars/token). This estimate uses "
|
||||
"3.5 so the research tables stay comparable. It is not a vendor tokenizer."
|
||||
)
|
||||
|
||||
REFERENCE_DATE = "2026-09-27"
|
||||
AYANAMSA = "raman"
|
||||
NODE_MODE = "mean"
|
||||
|
||||
QUESTIONS = (
|
||||
{"id": "career", "domain": "career", "question": "未来一年,事业和收入该关注什么?"},
|
||||
{"id": "marriage", "domain": "marriage", "question": "我的关系模式是什么?"},
|
||||
{"id": "wealth", "domain": "wealth", "question": "我的财富增长方式和风险点是什么?"},
|
||||
{"id": "health", "domain": "health", "question": "我近期的身心压力模式和调节重点是什么?"},
|
||||
{"id": "education", "domain": "education", "question": "我的学习优势、瓶颈和进阶方向是什么?"},
|
||||
{"id": "migration", "domain": "migration", "question": "迁居、置业或海外发展更适合怎样规划?"},
|
||||
{"id": "family", "domain": "family", "question": "我的家庭关系与责任边界该如何理解?"},
|
||||
{"id": "general", "domain": "general", "question": "请综合说明我当前最值得关注的主题。"},
|
||||
{"id": "parents", "domain": "family", "question": "我和父母关系如何"},
|
||||
{"id": "children", "domain": "family", "question": "我和子女的关系如何"},
|
||||
)
|
||||
|
||||
CHART_SOURCES = (
|
||||
{
|
||||
"id": "steve_jobs",
|
||||
"label": "Steve Jobs",
|
||||
"path": "references/real_case_calibration/minute_rectification_development_v1.json",
|
||||
"case_id": "steve_jobs_1955_development",
|
||||
},
|
||||
{
|
||||
"id": "barack_obama",
|
||||
"label": "Barack Obama",
|
||||
"path": "references/real_case_calibration/minute_rectification_holdout_v4.json",
|
||||
"case_id": "barack_obama_1961_aa_v4_holdout",
|
||||
},
|
||||
{
|
||||
"id": "elizabeth_taylor",
|
||||
"label": "Elizabeth Taylor",
|
||||
"path": "references/real_case_calibration/minute_rectification_holdout_v4.json",
|
||||
"case_id": "elizabeth_taylor_1932_aa_v4_holdout",
|
||||
},
|
||||
)
|
||||
|
||||
STATUS_TECHNIQUES = {
|
||||
"mevg / global web evidence",
|
||||
"real case calibration",
|
||||
"vedastro main entry overview",
|
||||
"vedastro cloud state",
|
||||
"external oracle progress",
|
||||
"interpretation source pack",
|
||||
"module execution audit",
|
||||
"exact cusp oracle",
|
||||
"blind technical mode",
|
||||
"timing precision gate",
|
||||
"evidence packet",
|
||||
"shadbala boundary",
|
||||
}
|
||||
RESEARCH_TECHNIQUES = {
|
||||
"research d-n through d60",
|
||||
"extended vargas d81/d108/d144",
|
||||
"ashtottari dasha",
|
||||
}
|
||||
NOT_APPLICABLE_TECHNIQUES = {
|
||||
"prashna chart",
|
||||
"ashtakoota matching",
|
||||
"adhana / niseka",
|
||||
"muhurta election",
|
||||
}
|
||||
WESTERN_PREFIX = "western"
|
||||
|
||||
STATUS_KEYS = {
|
||||
"methodology",
|
||||
"presentation",
|
||||
"rectification",
|
||||
"answer_policy",
|
||||
"user_facing_limitation",
|
||||
"hard_blockers",
|
||||
"missing_route_layers",
|
||||
"available_layers",
|
||||
"must_use_layers",
|
||||
"technique_truth",
|
||||
"reference_transparency",
|
||||
"shadbala_boundary",
|
||||
"vedastro_cross_check",
|
||||
"vedastro_gateway",
|
||||
"commercial_evidence_status",
|
||||
"success",
|
||||
"omitted_domains",
|
||||
"status",
|
||||
"packet_version",
|
||||
"schema_error",
|
||||
}
|
||||
|
||||
# Domain card draft. Values are copied from the projection; nothing is rewritten.
|
||||
CARD_SPECS: dict[str, dict[str, Any]] = {
|
||||
"career": {
|
||||
"label": "事业",
|
||||
"vargas": ["D10"],
|
||||
"houses": [10],
|
||||
"arudha": ["A10"],
|
||||
"karakas": ["AmK"],
|
||||
"planets": [],
|
||||
"yogas": True,
|
||||
"transits": True,
|
||||
"ashtakavarga": False,
|
||||
"shadbala": True,
|
||||
},
|
||||
"marriage": {
|
||||
"label": "婚恋",
|
||||
"vargas": ["D9"],
|
||||
"houses": [7],
|
||||
"arudha": ["UL", "A7"],
|
||||
"karakas": ["DK"],
|
||||
"planets": ["venus", "jupiter"],
|
||||
"yogas": False,
|
||||
"transits": True,
|
||||
"ashtakavarga": False,
|
||||
"shadbala": False,
|
||||
},
|
||||
"wealth": {
|
||||
"label": "财富",
|
||||
"vargas": ["D2", "D11"],
|
||||
"houses": [2, 11, 9, 5],
|
||||
"arudha": [],
|
||||
"karakas": [],
|
||||
"planets": [],
|
||||
"yogas": True,
|
||||
"transits": False,
|
||||
"ashtakavarga": True,
|
||||
"shadbala": True,
|
||||
},
|
||||
"health": {
|
||||
"label": "健康",
|
||||
"vargas": ["D6", "D8", "D30"],
|
||||
"houses": [6, 8],
|
||||
"arudha": [],
|
||||
"karakas": [],
|
||||
"planets": [],
|
||||
"yogas": False,
|
||||
"transits": True,
|
||||
"ashtakavarga": False,
|
||||
"shadbala": True,
|
||||
},
|
||||
"education": {
|
||||
"label": "学习",
|
||||
"vargas": ["D5", "D24"],
|
||||
"houses": [5, 9],
|
||||
"arudha": [],
|
||||
"karakas": [],
|
||||
"planets": ["mercury", "jupiter"],
|
||||
"yogas": False,
|
||||
"transits": False,
|
||||
"ashtakavarga": False,
|
||||
"shadbala": False,
|
||||
},
|
||||
"migration": {
|
||||
"label": "迁居",
|
||||
"vargas": ["D4", "D12"],
|
||||
"houses": [4, 12],
|
||||
"arudha": [],
|
||||
"karakas": [],
|
||||
"planets": [],
|
||||
"yogas": False,
|
||||
"transits": False,
|
||||
"ashtakavarga": False,
|
||||
"shadbala": False,
|
||||
},
|
||||
"family": {
|
||||
"label": "家庭(现路由,父母与子女混在一起)",
|
||||
"vargas": ["D7", "D12"],
|
||||
"houses": [4, 5, 9],
|
||||
"arudha": [],
|
||||
"karakas": [],
|
||||
"planets": [],
|
||||
"yogas": False,
|
||||
"transits": False,
|
||||
"ashtakavarga": False,
|
||||
"shadbala": False,
|
||||
},
|
||||
"parents": {
|
||||
"label": "父母",
|
||||
"vargas": ["D12"],
|
||||
"houses": [4, 9],
|
||||
"arudha": [],
|
||||
"karakas": [],
|
||||
"planets": ["sun", "moon"],
|
||||
"yogas": False,
|
||||
"transits": False,
|
||||
"ashtakavarga": False,
|
||||
"shadbala": False,
|
||||
"basis": [
|
||||
"skills/jyotish-vedic-astrology/references/strict-workflow-router.md shared baseline: D12 for family/ancestral themes",
|
||||
"references/birth-time-rectification-decision-tree.md section 4: 父母/家族 -> D12",
|
||||
],
|
||||
},
|
||||
"children": {
|
||||
"label": "子女",
|
||||
"vargas": ["D7"],
|
||||
"houses": [5],
|
||||
"arudha": [],
|
||||
"karakas": ["PK"],
|
||||
"planets": ["jupiter"],
|
||||
"yogas": False,
|
||||
"transits": False,
|
||||
"ashtakavarga": False,
|
||||
"shadbala": False,
|
||||
"basis": [
|
||||
"skills/jyotish-vedic-astrology/references/strict-workflow-router.md shared baseline: D7 for children",
|
||||
"references/birth-time-rectification-decision-tree.md section 4: 子女/生育 -> D7",
|
||||
],
|
||||
},
|
||||
"general": {
|
||||
"label": "综合",
|
||||
"vargas": ["D9", "D10", "D2"],
|
||||
"houses": [1, 10, 7, 2],
|
||||
"arudha": ["A10"],
|
||||
"karakas": [],
|
||||
"planets": [],
|
||||
"yogas": True,
|
||||
"transits": True,
|
||||
"ashtakavarga": True,
|
||||
"shadbala": True,
|
||||
},
|
||||
}
|
||||
|
||||
PART_B_BASE = (
|
||||
"functional_benefic_malefic",
|
||||
"vimshottari_md_ad_pd",
|
||||
"narayana_current",
|
||||
"ascendant_degree",
|
||||
)
|
||||
|
||||
|
||||
def dumps(value: Any) -> str:
|
||||
return json.dumps(value, ensure_ascii=False, separators=(",", ":"))
|
||||
|
||||
|
||||
def estimate_tokens(text: str) -> dict[str, Any]:
|
||||
chars = len(text)
|
||||
return {
|
||||
"chars": chars,
|
||||
"token_estimate": int(round(chars / TOKEN_CHARS_PER_TOKEN)),
|
||||
"token_method": TOKEN_METHOD,
|
||||
}
|
||||
|
||||
|
||||
def _norm(value: str) -> str:
|
||||
return re.sub(r"\s+", " ", value).strip().lower()
|
||||
|
||||
|
||||
def classify_audit_row(row: dict[str, Any]) -> str:
|
||||
technique = _norm(str(row.get("technique") or ""))
|
||||
system = _norm(str(row.get("system") or ""))
|
||||
status = _norm(str(row.get("status") or ""))
|
||||
if system == "western" or technique.startswith(WESTERN_PREFIX):
|
||||
return "western"
|
||||
if technique in RESEARCH_TECHNIQUES:
|
||||
return "research"
|
||||
if status == "not_applicable" or technique in NOT_APPLICABLE_TECHNIQUES:
|
||||
return "not_applicable"
|
||||
if technique in STATUS_TECHNIQUES or "vedastro" in technique or "mevg" in technique:
|
||||
return "status"
|
||||
return "core"
|
||||
|
||||
|
||||
def classify(path: tuple[str, ...], value: Any) -> str:
|
||||
joined = ".".join(path).lower()
|
||||
if "western_spectrum" in joined or joined.startswith("western"):
|
||||
return "western"
|
||||
if "research_dn" in joined or "extended" in joined and "varga" in joined:
|
||||
return "research"
|
||||
if isinstance(value, dict) and "technique" in value and "status" in value and "technique_audit" in joined:
|
||||
return classify_audit_row(value)
|
||||
leaf = path[-1].lower() if path else ""
|
||||
if leaf in STATUS_KEYS or leaf.endswith("_boundary") or leaf.endswith("_status"):
|
||||
return "status"
|
||||
if isinstance(value, str) and _norm(value) == "not_applicable":
|
||||
return "not_applicable"
|
||||
return "core"
|
||||
|
||||
|
||||
def _whole(path: tuple[str, ...], value: Any) -> str | None:
|
||||
joined = ".".join(path).lower()
|
||||
if joined.endswith("western_spectrum"):
|
||||
return "western"
|
||||
if joined.endswith("research_dn") or joined.endswith("extended") and "varga_spectrum" in joined:
|
||||
return "research"
|
||||
if joined.endswith("methodology") or joined.endswith("presentation") or joined.endswith("rectification"):
|
||||
return "status"
|
||||
if (
|
||||
isinstance(value, dict)
|
||||
and "technique" in value
|
||||
and "status" in value
|
||||
and any(part == "technique_audit_table" for part in path)
|
||||
):
|
||||
return classify_audit_row(value)
|
||||
return None
|
||||
|
||||
|
||||
def _dominant(sizes: Counter[str]) -> str:
|
||||
if not sizes:
|
||||
return "status"
|
||||
return max(CATEGORIES, key=lambda name: (sizes.get(name, 0), -CATEGORIES.index(name)))
|
||||
|
||||
|
||||
def account(value: Any, path: tuple[str, ...] = ()) -> Counter[str]:
|
||||
"""Partition dumps(value) across the five categories. Sum equals len(dumps(value))."""
|
||||
whole = _whole(path, value)
|
||||
if whole is not None:
|
||||
return Counter({whole: len(dumps(value))})
|
||||
if isinstance(value, dict):
|
||||
sizes: Counter[str] = Counter()
|
||||
if not value:
|
||||
sizes[classify(path, value)] = 2
|
||||
return sizes
|
||||
items = list(value.items())
|
||||
for index, (key, child) in enumerate(items):
|
||||
child_path = path + (str(key),)
|
||||
child_sizes = account(child, child_path)
|
||||
sizes.update(child_sizes)
|
||||
overhead = len(dumps(str(key))) + 1
|
||||
if index:
|
||||
overhead += 1
|
||||
sizes[_dominant(child_sizes)] += overhead
|
||||
sizes[_dominant(sizes)] += 2
|
||||
return sizes
|
||||
if isinstance(value, list):
|
||||
sizes = Counter()
|
||||
if not value:
|
||||
sizes[classify(path, value)] = 2
|
||||
return sizes
|
||||
for index, child in enumerate(value):
|
||||
child_sizes = account(child, path + (str(index),))
|
||||
sizes.update(child_sizes)
|
||||
if index:
|
||||
sizes[_dominant(child_sizes)] += 1
|
||||
sizes[_dominant(sizes)] += 2
|
||||
return sizes
|
||||
return Counter({classify(path, value): len(dumps(value))})
|
||||
|
||||
|
||||
def category_report(value: Any) -> dict[str, Any]:
|
||||
sizes = account(value)
|
||||
total = len(dumps(value))
|
||||
covered = sum(sizes[name] for name in CATEGORIES)
|
||||
return {
|
||||
"categories": {name: int(sizes.get(name, 0)) for name in CATEGORIES},
|
||||
"total_chars": total,
|
||||
"category_sum": covered,
|
||||
"sum_error_ratio": 0.0 if total == 0 else abs(covered - total) / total,
|
||||
"tokens": estimate_tokens(dumps(value)),
|
||||
}
|
||||
|
||||
|
||||
def duplicate_report(model_context: dict[str, Any]) -> dict[str, Any]:
|
||||
consultations = model_context.get("consultations")
|
||||
first = consultations[0] if isinstance(consultations, list) and consultations else None
|
||||
spread_keys = [
|
||||
key for key in (first or {})
|
||||
if key in model_context and key != "consultations"
|
||||
]
|
||||
matching = [
|
||||
key for key in spread_keys
|
||||
if dumps(model_context.get(key)) == dumps(first.get(key))
|
||||
]
|
||||
named_copies: dict[str, int] = {}
|
||||
|
||||
def walk(node: Any, path: tuple[str, ...] = ()) -> None:
|
||||
if isinstance(node, dict):
|
||||
for key, child in node.items():
|
||||
leaf = str(key)
|
||||
if leaf in {"varga_spectrum", "western_spectrum", "technique_audit_table", "methodology"}:
|
||||
named_copies[leaf] = named_copies.get(leaf, 0) + 1
|
||||
walk(child, path + (leaf,))
|
||||
elif isinstance(node, list):
|
||||
for index, child in enumerate(node):
|
||||
walk(child, path + (str(index),))
|
||||
|
||||
walk(model_context)
|
||||
consultation_chars = len(dumps(consultations)) if consultations is not None else 0
|
||||
return {
|
||||
"consultation_copies": 1 + (1 if isinstance(first, dict) else 0),
|
||||
"spread_keys_matching_consultations_0": matching,
|
||||
"consultations_chars": consultation_chars,
|
||||
"named_subtree_copies": named_copies,
|
||||
}
|
||||
|
||||
|
||||
def load_public_charts(root: Path) -> list[dict[str, Any]]:
|
||||
charts = []
|
||||
for spec in CHART_SOURCES:
|
||||
payload = json.loads((root / spec["path"]).read_text(encoding="utf-8"))
|
||||
case = next(item for item in payload["cases"] if item["case_id"] == spec["case_id"])
|
||||
birth = case["birth"]
|
||||
year, month, day = (int(part) for part in birth["date"].split("-"))
|
||||
hour, minute = (int(part) for part in birth["time"].split(":")[:2])
|
||||
charts.append({
|
||||
"id": spec["id"],
|
||||
"label": spec["label"],
|
||||
"source": spec["path"],
|
||||
"case_id": spec["case_id"],
|
||||
"rodden_rating": (birth.get("source") or {}).get("rodden_rating"),
|
||||
"place": birth.get("place"),
|
||||
"body": {
|
||||
"year": year,
|
||||
"month": month,
|
||||
"day": day,
|
||||
"hour": hour,
|
||||
"minute": minute,
|
||||
"second": 0,
|
||||
"lat": birth["latitude"],
|
||||
"lon": birth["longitude"],
|
||||
"tz": birth["timezone_offset"],
|
||||
"city": birth.get("place") or spec["label"],
|
||||
"ayanamsa": AYANAMSA,
|
||||
"node_mode": NODE_MODE,
|
||||
"today": REFERENCE_DATE,
|
||||
"entry_mode": "direct_chart",
|
||||
"defer_optional_external_evidence": True,
|
||||
"declared_accuracy": "minute",
|
||||
"birth_time_accuracy": "confirmed",
|
||||
},
|
||||
})
|
||||
return charts
|
||||
|
||||
|
||||
def _dig(value: Any, *keys: str) -> Any:
|
||||
current = value
|
||||
for key in keys:
|
||||
if not isinstance(current, dict) or key not in current:
|
||||
return None
|
||||
current = current[key]
|
||||
return current
|
||||
|
||||
|
||||
def claim_evidence(model_context: dict[str, Any], category: str) -> dict[str, Any]:
|
||||
cards = model_context.get("claim_cards")
|
||||
if not isinstance(cards, list):
|
||||
return {}
|
||||
for card in cards:
|
||||
if isinstance(card, dict) and card.get("category") == category and isinstance(card.get("evidence"), dict):
|
||||
return card["evidence"]
|
||||
return {}
|
||||
|
||||
|
||||
def _house_entry(houses: Any, number: int) -> Any:
|
||||
if isinstance(houses, dict):
|
||||
for key in (str(number), f"house_{number}", f"h{number}"):
|
||||
if key in houses:
|
||||
return houses[key]
|
||||
for value in houses.values():
|
||||
if isinstance(value, dict) and value.get("number") == number:
|
||||
return value
|
||||
if isinstance(houses, list):
|
||||
for value in houses:
|
||||
if isinstance(value, dict) and value.get("number") == number:
|
||||
return value
|
||||
if 1 <= number <= len(houses):
|
||||
return houses[number - 1]
|
||||
return None
|
||||
|
||||
|
||||
def _pick(mapping: Any, names: list[str]) -> dict[str, Any]:
|
||||
if not isinstance(mapping, dict):
|
||||
return {}
|
||||
lowered = {str(key).lower(): key for key in mapping}
|
||||
picked = {}
|
||||
for name in names:
|
||||
key = lowered.get(name.lower())
|
||||
if key is not None:
|
||||
picked[str(key)] = mapping[key]
|
||||
return picked
|
||||
|
||||
|
||||
def cut_evidence_card(model_context: dict[str, Any], question_id: str) -> dict[str, Any]:
|
||||
"""Copy card fields from the projected model context. Missing paths stay gaps."""
|
||||
spec = CARD_SPECS[question_id]
|
||||
natal = claim_evidence(model_context, "natal_foundation")
|
||||
timing = claim_evidence(model_context, "timing")
|
||||
spectrum = natal.get("varga_spectrum") if isinstance(natal.get("varga_spectrum"), dict) else {}
|
||||
formal = spectrum.get("formal") if isinstance(spectrum.get("formal"), dict) else {}
|
||||
houses = natal.get("houses")
|
||||
planets = natal.get("planets") if isinstance(natal.get("planets"), dict) else {}
|
||||
functional = natal.get("functional_benefic_malefic")
|
||||
dasha = timing.get("dasha_sub_periods") if isinstance(timing.get("dasha_sub_periods"), dict) else timing.get("dasha")
|
||||
narayana = timing.get("narayana_dasha")
|
||||
gaps = []
|
||||
vargas = {}
|
||||
for code in spec["vargas"]:
|
||||
chart = formal.get(code)
|
||||
if chart is None:
|
||||
gaps.append(f"varga:{code}")
|
||||
else:
|
||||
vargas[code] = chart
|
||||
house_rows = {}
|
||||
for number in spec["houses"]:
|
||||
row = _house_entry(houses, number)
|
||||
if row is None:
|
||||
gaps.append(f"house:{number}")
|
||||
else:
|
||||
house_rows[str(number)] = row
|
||||
picked_planets = _pick(planets, spec["planets"])
|
||||
for name in spec["planets"]:
|
||||
if name.lower() not in {key.lower() for key in picked_planets}:
|
||||
gaps.append(f"planet:{name}")
|
||||
card = {
|
||||
"card_id": question_id,
|
||||
"label": spec["label"],
|
||||
"question": model_context.get("question"),
|
||||
"route": model_context.get("route"),
|
||||
"base": {
|
||||
"ascendant": natal.get("ascendant"),
|
||||
"houses": houses,
|
||||
"planet_placements": _planet_placements(planets),
|
||||
"functional_benefic_malefic": functional,
|
||||
"vimshottari": dasha,
|
||||
"narayana_dasha": narayana,
|
||||
},
|
||||
"domain": {
|
||||
"vargas": vargas,
|
||||
"houses": house_rows,
|
||||
"planets": picked_planets,
|
||||
"arudha": _pick(natal.get("arudha_padas"), spec["arudha"]),
|
||||
"karakas": _pick(_dig(natal, "jaimini", "chara_karakas") or natal.get("chara_karakas"), spec["karakas"]),
|
||||
"yogas": natal.get("yogas") if spec["yogas"] else None,
|
||||
"transits": timing.get("transits") if spec["transits"] else None,
|
||||
"ashtakavarga": natal.get("ashtakavarga") if spec["ashtakavarga"] else None,
|
||||
"shadbala": natal.get("shadbala") if spec["shadbala"] else None,
|
||||
},
|
||||
"gaps": gaps,
|
||||
"basis": spec.get("basis", []),
|
||||
}
|
||||
if card["base"]["ascendant"] is None:
|
||||
gaps.append("ascendant")
|
||||
if functional is None:
|
||||
gaps.append("functional_benefic_malefic")
|
||||
if dasha is None:
|
||||
gaps.append("vimshottari")
|
||||
if narayana is None:
|
||||
gaps.append("narayana_dasha")
|
||||
text = dumps(card)
|
||||
return {"card": card, "text": text, "size": estimate_tokens(text), "gaps": gaps}
|
||||
|
||||
|
||||
def _ci_get(mapping: Any, name: str) -> Any:
|
||||
if not isinstance(mapping, dict):
|
||||
return None
|
||||
for key, value in mapping.items():
|
||||
if str(key).lower() == name.lower():
|
||||
return value
|
||||
return None
|
||||
|
||||
|
||||
def _planet_placements(planets: Any) -> dict[str, Any]:
|
||||
if not isinstance(planets, dict):
|
||||
return {}
|
||||
placed = {}
|
||||
for name, row in planets.items():
|
||||
if isinstance(row, dict):
|
||||
placed[str(name)] = {
|
||||
key: row[key] for key in ("sign", "house") if key in row
|
||||
}
|
||||
elif isinstance(row, str):
|
||||
placed[str(name)] = {"sign": row}
|
||||
return placed
|
||||
|
||||
|
||||
def engine_facts(workflow: dict[str, Any]) -> dict[str, Any]:
|
||||
chart = workflow.get("chart") if isinstance(workflow.get("chart"), dict) else {}
|
||||
modules = chart.get("modules") if isinstance(chart.get("modules"), dict) else {}
|
||||
ascendant = chart.get("ascendant") if isinstance(chart.get("ascendant"), dict) else {}
|
||||
planets = chart.get("planets") if isinstance(chart.get("planets"), dict) else {}
|
||||
moon = _ci_get(planets, "moon")
|
||||
moon = moon if isinstance(moon, dict) else {}
|
||||
sub = modules.get("dasha_sub_periods") if isinstance(modules.get("dasha_sub_periods"), dict) else {}
|
||||
current = sub.get("current") if isinstance(sub.get("current"), dict) else {}
|
||||
mahadasha = current.get("mahadasha") if isinstance(current.get("mahadasha"), dict) else {}
|
||||
antardasha = current.get("antardasha") if isinstance(current.get("antardasha"), dict) else {}
|
||||
pratyantar = sub.get("pratyantar_dasha_timeline") if isinstance(sub.get("pratyantar_dasha_timeline"), dict) else {}
|
||||
pratyantar_current = pratyantar.get("current") if isinstance(pratyantar.get("current"), dict) else {}
|
||||
narayana = modules.get("narayana_dasha") if isinstance(modules.get("narayana_dasha"), dict) else {}
|
||||
narayana_current = narayana.get("current_dasha") if isinstance(narayana.get("current_dasha"), dict) else {}
|
||||
narayana_md = narayana_current.get("md") if isinstance(narayana_current.get("md"), dict) else {}
|
||||
spectrum = modules.get("varga_spectrum") if isinstance(modules.get("varga_spectrum"), dict) else {}
|
||||
formal = spectrum.get("formal") if isinstance(spectrum.get("formal"), dict) else {}
|
||||
d12 = formal.get("D12") if isinstance(formal.get("D12"), dict) else {}
|
||||
return {
|
||||
"ascendant_sign": ascendant.get("sign"),
|
||||
"moon_sign": moon.get("sign"),
|
||||
"moon_house": moon.get("house"),
|
||||
"vimshottari_lord": mahadasha.get("lord"),
|
||||
"vimshottari_start": mahadasha.get("start"),
|
||||
"vimshottari_end": mahadasha.get("end"),
|
||||
"antardasha_start": antardasha.get("start"),
|
||||
"antardasha_end": antardasha.get("end"),
|
||||
"pratyantar_start": pratyantar_current.get("start"),
|
||||
"pratyantar_end": pratyantar_current.get("end"),
|
||||
"narayana_sign": narayana_md.get("sign"),
|
||||
"d12_lagna": d12.get("lagna") if isinstance(d12.get("lagna"), str) else None,
|
||||
"d12_moon": _ci_get(d12.get("planets"), "moon") if isinstance(d12.get("planets"), dict) else None,
|
||||
}
|
||||
|
||||
|
||||
def card_facts(card: dict[str, Any]) -> dict[str, Any]:
|
||||
base = card.get("base") if isinstance(card.get("base"), dict) else {}
|
||||
domain = card.get("domain") if isinstance(card.get("domain"), dict) else {}
|
||||
ascendant = base.get("ascendant") if isinstance(base.get("ascendant"), dict) else {}
|
||||
moon = _ci_get(base.get("planet_placements"), "moon")
|
||||
if not isinstance(moon, dict):
|
||||
moon = _ci_get(domain.get("planets"), "moon")
|
||||
moon = moon if isinstance(moon, dict) else {}
|
||||
sub = base.get("vimshottari") if isinstance(base.get("vimshottari"), dict) else {}
|
||||
current = sub.get("current") if isinstance(sub.get("current"), dict) else {}
|
||||
mahadasha = current.get("mahadasha") if isinstance(current.get("mahadasha"), dict) else {}
|
||||
antardasha = current.get("antardasha") if isinstance(current.get("antardasha"), dict) else {}
|
||||
pratyantar = sub.get("pratyantar_dasha_timeline") if isinstance(sub.get("pratyantar_dasha_timeline"), dict) else {}
|
||||
pratyantar_current = pratyantar.get("current") if isinstance(pratyantar.get("current"), dict) else {}
|
||||
narayana = base.get("narayana_dasha") if isinstance(base.get("narayana_dasha"), dict) else {}
|
||||
narayana_current = narayana.get("current_dasha") if isinstance(narayana.get("current_dasha"), dict) else {}
|
||||
narayana_md = narayana_current.get("md") if isinstance(narayana_current.get("md"), dict) else {}
|
||||
d12 = (domain.get("vargas") or {}).get("D12") if isinstance(domain.get("vargas"), dict) else None
|
||||
d12 = d12 if isinstance(d12, dict) else {}
|
||||
return {
|
||||
"ascendant_sign": ascendant.get("sign"),
|
||||
"moon_sign": moon.get("sign"),
|
||||
"moon_house": moon.get("house"),
|
||||
"vimshottari_lord": mahadasha.get("lord"),
|
||||
"vimshottari_start": mahadasha.get("start"),
|
||||
"vimshottari_end": mahadasha.get("end"),
|
||||
"antardasha_start": antardasha.get("start"),
|
||||
"antardasha_end": antardasha.get("end"),
|
||||
"pratyantar_start": pratyantar_current.get("start"),
|
||||
"pratyantar_end": pratyantar_current.get("end"),
|
||||
"narayana_sign": narayana_md.get("sign"),
|
||||
"d12_lagna": d12.get("lagna"),
|
||||
"d12_moon": _ci_get(d12.get("planets"), "moon"),
|
||||
}
|
||||
|
||||
|
||||
def verbatim_check(card: dict[str, Any], facts: dict[str, Any]) -> dict[str, Any]:
|
||||
actual = card_facts(card)
|
||||
has_d12 = isinstance((card.get("domain") or {}).get("vargas"), dict) and "D12" in card["domain"]["vargas"]
|
||||
checks = {}
|
||||
for key, expected in facts.items():
|
||||
if key.startswith("d12_") and not has_d12:
|
||||
checks[key] = {"status": "not_on_card", "expected": expected, "actual": None}
|
||||
continue
|
||||
got = actual.get(key)
|
||||
if expected is None or expected == "":
|
||||
checks[key] = {"status": "engine_missing", "expected": None, "actual": got}
|
||||
elif got == expected:
|
||||
checks[key] = {"status": "match", "expected": expected, "actual": got}
|
||||
else:
|
||||
checks[key] = {"status": "mismatch", "expected": expected, "actual": got}
|
||||
mismatched = [key for key, row in checks.items() if row["status"] == "mismatch"]
|
||||
return {"ok": not mismatched, "checks": checks, "mismatched": mismatched}
|
||||
|
||||
|
||||
def parse_registry_layers(source: str) -> dict[str, list[str]]:
|
||||
found = {}
|
||||
for match in re.finditer(r'id: "(\w+)".*?requiredLayers: \[([^\]]*)\]', source):
|
||||
layers = re.findall(r'"([^"]+)"', match.group(2))
|
||||
found[match.group(1)] = layers
|
||||
return found
|
||||
|
||||
|
||||
def parse_methodology_routes(source: str) -> dict[str, str | None]:
|
||||
found = {}
|
||||
for match in re.finditer(r'(\w+): \{ strictRoute: (?:"([^"]+)"|null)', source):
|
||||
found[match.group(1)] = match.group(2)
|
||||
return found
|
||||
|
||||
|
||||
def parse_must_use_layers(source: str) -> dict[str, list[str]]:
|
||||
block = re.search(r"DOMAIN_MUST_USE_LAYERS[\s\S]*?};", source)
|
||||
if not block:
|
||||
return {}
|
||||
found = {}
|
||||
for match in re.finditer(r'(\w+): \[([^\]]*)\]', block.group(0)):
|
||||
found[match.group(1)] = re.findall(r'"([^"]+)"', match.group(2))
|
||||
return found
|
||||
|
||||
|
||||
def source_matrix(root: Path) -> dict[str, Any]:
|
||||
registry = parse_registry_layers((root / "frontend/src/lib/consultation-domain-registry.ts").read_text(encoding="utf-8"))
|
||||
methodology = parse_methodology_routes((root / "frontend/src/lib/consultation-methodology.ts").read_text(encoding="utf-8"))
|
||||
must_use = parse_must_use_layers((root / "frontend/src/mastra/consultation-workflow.ts").read_text(encoding="utf-8"))
|
||||
from unified_consultation_orchestrator import UnifiedConsultationOrchestrator
|
||||
|
||||
python_routes = {
|
||||
name: list(route.focus_techniques)
|
||||
for name, route in UnifiedConsultationOrchestrator._ROUTE_DEFINITIONS.items()
|
||||
}
|
||||
domains = sorted(set(registry) | set(methodology) | set(python_routes) | set(CARD_SPECS))
|
||||
rows = []
|
||||
for domain in domains:
|
||||
card_id = domain if domain in CARD_SPECS else None
|
||||
rows.append({
|
||||
"domain": domain,
|
||||
"registry_required_layers": registry.get(domain, []),
|
||||
"methodology_strict_route": methodology.get(domain),
|
||||
"ts_must_use_layers": must_use.get(domain, []),
|
||||
"python_focus_techniques": python_routes.get(domain, []),
|
||||
"card_vargas": CARD_SPECS.get(card_id, {}).get("vargas", []) if card_id else [],
|
||||
"parents_children_split": domain == "family",
|
||||
})
|
||||
return {"domains": rows, "skill_citations": {
|
||||
"d12_family": "references/strict-workflow-router.md: D12 for family/ancestral themes",
|
||||
"d7_children": "references/strict-workflow-router.md: D7 for children",
|
||||
"decision_tree_parents": "references/birth-time-rectification-decision-tree.md: 父母/家族 -> D12",
|
||||
"decision_tree_children": "references/birth-time-rectification-decision-tree.md: 子女/生育 -> D7",
|
||||
}}
|
||||
|
||||
|
||||
def part_b_coverage(card: dict[str, Any], question_id: str) -> dict[str, Any]:
|
||||
base = card.get("base") if isinstance(card.get("base"), dict) else {}
|
||||
domain = card.get("domain") if isinstance(card.get("domain"), dict) else {}
|
||||
spec = CARD_SPECS[question_id]
|
||||
present = {
|
||||
"functional_benefic_malefic": base.get("functional_benefic_malefic") is not None,
|
||||
"vimshottari": base.get("vimshottari") is not None,
|
||||
"narayana": base.get("narayana_dasha") is not None,
|
||||
"ascendant_raw": base.get("ascendant") is not None,
|
||||
"domain_vargas": sorted(domain.get("vargas") or {}),
|
||||
"expected_vargas": spec["vargas"],
|
||||
}
|
||||
missing_vargas = [code for code in spec["vargas"] if code not in (domain.get("vargas") or {})]
|
||||
return {
|
||||
"dual_dasha": present["vimshottari"] and present["narayana"],
|
||||
"domain_divisional": not missing_vargas,
|
||||
"functional_benefic_malefic": present["functional_benefic_malefic"],
|
||||
"raw_data": present["ascendant_raw"] and present["vimshottari"],
|
||||
"mevg": "kept_backstage",
|
||||
"real_case_calibration": "kept_backstage",
|
||||
"missing_vargas": missing_vargas,
|
||||
"present": present,
|
||||
}
|
||||
|
||||
|
||||
TELEMETRY_SPEC = {
|
||||
"stores": "numbers, enums, and field names only",
|
||||
"does_not_store": [
|
||||
"question text",
|
||||
"answer text",
|
||||
"birth date",
|
||||
"birth time",
|
||||
"birth place",
|
||||
"name",
|
||||
"email",
|
||||
"user id",
|
||||
"session id",
|
||||
"matched substring",
|
||||
],
|
||||
"fields": [
|
||||
{"name": "domain", "type": "enum", "values": [item["id"] for item in QUESTIONS]},
|
||||
{"name": "card_version", "type": "enum", "values": ["evidence-card-draft-20260927"]},
|
||||
{"name": "card_chars", "type": "int"},
|
||||
{"name": "card_token_estimate", "type": "int"},
|
||||
{"name": "cited_field_ids", "type": "enum[]", "note": "ids from the card schema, not free text"},
|
||||
{"name": "feedback", "type": "enum", "values": ["up", "down", "none"]},
|
||||
],
|
||||
"citation_method": {
|
||||
"rule": "A field is cited when its scalar value appears in the answer and the value is an ISO date, a degree token, or a compound planet-in-sign phrase of at least 8 characters. Bare planet or sign names are not counted.",
|
||||
"false_positive": "A date or degree that also appears in the question, or a repeated stock phrase, can be counted without the model using that field.",
|
||||
"false_negative": "A paraphrase that names the period without copying the date is not counted.",
|
||||
"stored": "Only the field id. The matched text is discarded.",
|
||||
},
|
||||
"aggregates": [
|
||||
"count of answers by domain and card_version",
|
||||
"fields whose cited rate stays under 5 percent after 30 answers in that domain",
|
||||
"down rate by domain and card_version",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def card_dependencies(question_id: str) -> list[str]:
|
||||
spec = CARD_SPECS[question_id]
|
||||
modules = ["compute_chart", "functional_benefic_malefic", "dasha_sub_periods", "narayana_dasha"]
|
||||
if spec["vargas"]:
|
||||
modules.append("varga_requested_only")
|
||||
if spec["arudha"] or spec["karakas"]:
|
||||
modules.append("arudha_padas")
|
||||
if spec["karakas"]:
|
||||
modules.append("chara_karakas")
|
||||
if spec["transits"]:
|
||||
modules.append("transits")
|
||||
if spec["ashtakavarga"]:
|
||||
modules.append("ashtakavarga")
|
||||
if spec["shadbala"]:
|
||||
modules.append("shadbala")
|
||||
if spec["yogas"]:
|
||||
modules.append("yogas")
|
||||
return modules
|
||||
@@ -0,0 +1,263 @@
|
||||
"""Run the consultation evidence-card inventory. Read-only toward the engine.
|
||||
|
||||
External VedAstro is not called. The product foreground path still joins a
|
||||
background gateway; this process replaces that join with the engine's own
|
||||
blocked payload so local timing is not a network wait. Status text from that
|
||||
blocked payload is what the model sees when the gateway does not return.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
SCRIPTS = ROOT / "scripts"
|
||||
RESEARCH = Path(__file__).resolve().parent
|
||||
for path in (ROOT, SCRIPTS, RESEARCH):
|
||||
if str(path) not in sys.path:
|
||||
sys.path.insert(0, str(path))
|
||||
|
||||
from consult_evidence_card_lib import ( # noqa: E402
|
||||
QUESTIONS,
|
||||
card_dependencies,
|
||||
category_report,
|
||||
cut_evidence_card,
|
||||
duplicate_report,
|
||||
engine_facts,
|
||||
estimate_tokens,
|
||||
load_public_charts,
|
||||
part_b_coverage,
|
||||
source_matrix,
|
||||
verbatim_check,
|
||||
)
|
||||
|
||||
|
||||
def _block_external_vedastro() -> None:
|
||||
import vedastro_foreground as gateway
|
||||
|
||||
def start(self):
|
||||
self.meta = {
|
||||
"freshness": "research_not_called",
|
||||
"external_vedastro": "not_called",
|
||||
}
|
||||
self.cached_gateway = gateway._blocked_foreground_vedastro(
|
||||
reason="research_external_vedastro_not_called",
|
||||
)
|
||||
self.future = None
|
||||
return self
|
||||
|
||||
gateway.ForegroundVedastroSession.start = start
|
||||
|
||||
|
||||
def _workflow_body(chart: dict[str, Any], question: dict[str, str]) -> dict[str, Any]:
|
||||
from consultation_plan_contract import PLAN_VERSION, plan_route_contract
|
||||
|
||||
domain = question["domain"]
|
||||
contract = plan_route_contract(domain)
|
||||
if contract is None:
|
||||
raise RuntimeError(f"no plan contract for {domain}")
|
||||
body = dict(chart["body"])
|
||||
body.update({
|
||||
"question": question["question"],
|
||||
"question_text": question["question"],
|
||||
"theme": list(contract.themes),
|
||||
"plan_version": PLAN_VERSION,
|
||||
"strict_workflow_route": domain,
|
||||
"required_layers": list(contract.required_layers),
|
||||
"claim_boundary": contract.claim_boundary,
|
||||
"plan_depth": "standard",
|
||||
"requested_domains": list(contract.requested_domains),
|
||||
"timing_horizon": None,
|
||||
"precision_boundary": "server_evidence_required",
|
||||
"required_evidence_categories": list(contract.required_evidence_categories),
|
||||
"surface": "api_web",
|
||||
})
|
||||
return body
|
||||
|
||||
|
||||
def _run_workflow(body: dict[str, Any]) -> dict[str, Any]:
|
||||
from jyotish_api_server import JyotishAPIHandler
|
||||
|
||||
handler = JyotishAPIHandler.__new__(JyotishAPIHandler)
|
||||
return handler._compute_consultation_workflow(body)
|
||||
|
||||
|
||||
def _project(jobs: list[dict[str, str]], summary_path: Path) -> list[dict[str, Any]]:
|
||||
spec_path = summary_path.with_suffix(".jobs.json")
|
||||
spec_path.write_text(json.dumps({"items": jobs}), encoding="utf-8")
|
||||
tsx = ROOT / "frontend" / "node_modules" / "tsx" / "dist" / "cli.mjs"
|
||||
env = os.environ.copy()
|
||||
env["JYOTISHA_PROJECT_ROOT"] = str(ROOT)
|
||||
command = [
|
||||
"node",
|
||||
str(tsx),
|
||||
str(ROOT / "scripts" / "research" / "project_consult_model_context.ts"),
|
||||
"project",
|
||||
str(spec_path),
|
||||
str(summary_path),
|
||||
]
|
||||
completed = subprocess.run(command, cwd=ROOT, env=env, capture_output=True, text=True)
|
||||
if completed.returncode != 0:
|
||||
raise RuntimeError(completed.stderr[-4000:] or completed.stdout[-4000:])
|
||||
return json.loads(summary_path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def _time_layers(chart_body: dict[str, Any]) -> dict[str, float]:
|
||||
"""Time local layers on one birth payload. VedAstro stays blocked."""
|
||||
from jyotish_api_server import (
|
||||
JyotishAPIHandler,
|
||||
_attach_local_consultation_layers,
|
||||
_build_consultation_varga_spectrum,
|
||||
_custom_research_dn_chart,
|
||||
_planet_longitudes,
|
||||
)
|
||||
|
||||
handler = JyotishAPIHandler.__new__(JyotishAPIHandler)
|
||||
timings: dict[str, float] = {}
|
||||
|
||||
def elapsed(name: str, fn, *args, **kwargs):
|
||||
started = time.perf_counter()
|
||||
result = fn(*args, **kwargs)
|
||||
timings[name] = round(time.perf_counter() - started, 4)
|
||||
return result
|
||||
|
||||
chart = elapsed("compute_chart", handler._compute_chart, {
|
||||
**chart_body,
|
||||
"skip_vedastro_main_entry_overview": True,
|
||||
})
|
||||
planets = chart.get("planets") if isinstance(chart, dict) else {}
|
||||
ascendant = chart.get("ascendant") if isinstance(chart, dict) else {}
|
||||
elapsed("attach_local_layers", _attach_local_consultation_layers, handler, chart, chart_body, chart_body)
|
||||
if isinstance(planets, dict) and isinstance(ascendant, dict) and planets and ascendant:
|
||||
elapsed("varga_spectrum", _build_consultation_varga_spectrum, handler, planets, ascendant)
|
||||
elapsed("shadbala", handler._compute_shadbala, {**chart_body, "planets": planets, "ascendant": ascendant})
|
||||
elapsed("ashtakavarga", handler._compute_ashtakavarga, {"planets": planets, "ascendant": ascendant})
|
||||
elapsed("functional_benefic_malefic", handler._functional_benefic_malefic_snapshot, planets, ascendant)
|
||||
try:
|
||||
module = __import__("divisional_charts_extended", fromlist=["DivisionalChartsCalculator"])
|
||||
calc = module.DivisionalChartsCalculator()
|
||||
planet_lons = _planet_longitudes(planets)
|
||||
asc_lon = float(ascendant.get("lon"))
|
||||
elapsed("one_research_division", _custom_research_dn_chart, calc, planet_lons, asc_lon, 3)
|
||||
except Exception as exc:
|
||||
timings["one_research_division_error"] = exc.__class__.__name__
|
||||
return timings
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--out", default=str(ROOT / "docs" / "research" / "consult_evidence_card_inventory_2026_09_27.json"))
|
||||
parser.add_argument("--cache", default="")
|
||||
parser.add_argument("--only-chart", action="append", default=[])
|
||||
parser.add_argument("--only-question", action="append", default=[])
|
||||
parser.add_argument("--skip-timing", action="store_true")
|
||||
args = parser.parse_args()
|
||||
if os.environ.get("PYTHONHASHSEED") != "0":
|
||||
raise SystemExit("Set PYTHONHASHSEED=0 before starting this process (ERR-111).")
|
||||
|
||||
_block_external_vedastro()
|
||||
cache = Path(args.cache) if args.cache else Path(os.environ.get("TEMP", ".")) / "consult-evidence-card-20260927"
|
||||
cache.mkdir(parents=True, exist_ok=True)
|
||||
charts = load_public_charts(ROOT)
|
||||
questions = list(QUESTIONS)
|
||||
if args.only_chart:
|
||||
charts = [chart for chart in charts if chart["id"] in args.only_chart]
|
||||
if args.only_question:
|
||||
questions = [item for item in questions if item["id"] in args.only_question]
|
||||
|
||||
runs = []
|
||||
jobs = []
|
||||
for chart in charts:
|
||||
for question in questions:
|
||||
run_id = f"{chart['id']}__{question['id']}"
|
||||
workflow_path = cache / f"{run_id}.workflow.json"
|
||||
started = time.perf_counter()
|
||||
workflow = _run_workflow(_workflow_body(chart, question))
|
||||
wall = round(time.perf_counter() - started, 3)
|
||||
workflow_path.write_text(json.dumps(workflow, ensure_ascii=False), encoding="utf-8")
|
||||
projected_path = cache / f"{run_id}.model.json"
|
||||
jobs.append({
|
||||
"id": run_id,
|
||||
"domain": question["domain"],
|
||||
"question": question["question"],
|
||||
"workflow_path": str(workflow_path),
|
||||
"out_path": str(projected_path),
|
||||
})
|
||||
runs.append({
|
||||
"id": run_id,
|
||||
"chart_id": chart["id"],
|
||||
"chart_label": chart["label"],
|
||||
"question_id": question["id"],
|
||||
"domain": question["domain"],
|
||||
"wall_seconds": wall,
|
||||
"workflow_chars": workflow_path.stat().st_size,
|
||||
"route": (workflow.get("routing") or {}).get("question_type"),
|
||||
"focus_techniques": (workflow.get("routing") or {}).get("focus_techniques"),
|
||||
"success": workflow.get("success"),
|
||||
})
|
||||
print(f"{run_id} wall={wall}s route={runs[-1]['route']}", flush=True)
|
||||
|
||||
summary_path = cache / "projection-summary.json"
|
||||
projected = {item["id"]: item for item in _project(jobs, summary_path)}
|
||||
inventory = []
|
||||
for run, job in zip(runs, jobs):
|
||||
model_context = json.loads(Path(job["out_path"]).read_text(encoding="utf-8"))
|
||||
workflow = json.loads(Path(job["workflow_path"]).read_text(encoding="utf-8"))
|
||||
categories = category_report(model_context)
|
||||
card = cut_evidence_card(model_context, run["question_id"])
|
||||
facts = engine_facts(workflow)
|
||||
check = verbatim_check(card["card"], facts)
|
||||
inventory.append({
|
||||
**run,
|
||||
"projection": projected.get(run["id"]),
|
||||
"categories": categories,
|
||||
"duplicates": duplicate_report(model_context),
|
||||
"card_size": card["size"],
|
||||
"card_gaps": card["gaps"],
|
||||
"part_b": part_b_coverage(card["card"], run["question_id"]),
|
||||
"verbatim": check,
|
||||
"card_dependencies": card_dependencies(run["question_id"]),
|
||||
"sample_card": card["card"] if run["chart_id"] == "steve_jobs" and run["question_id"] in {"parents", "career"} else None,
|
||||
})
|
||||
|
||||
timings = {}
|
||||
if not args.skip_timing:
|
||||
for chart in charts:
|
||||
print(f"timing {chart['id']}", flush=True)
|
||||
try:
|
||||
timings[chart["id"]] = _time_layers(chart["body"])
|
||||
except Exception as exc:
|
||||
timings[chart["id"]] = {"error": exc.__class__.__name__, "message": str(exc)[:300]}
|
||||
|
||||
payload = {
|
||||
"baseline": "76924e3362c0a47d9d8de896c448c66055826f70",
|
||||
"reference_date": "2026-09-27",
|
||||
"ayanamsa": "raman",
|
||||
"node_mode": "mean",
|
||||
"pythonhashseed": "0",
|
||||
"external_vedastro": "not_called",
|
||||
"token_method": estimate_tokens("")["token_method"],
|
||||
"charts": [
|
||||
{"id": chart["id"], "label": chart["label"], "source": chart["source"], "case_id": chart["case_id"], "rodden_rating": chart["rodden_rating"]}
|
||||
for chart in load_public_charts(ROOT)
|
||||
],
|
||||
"source_matrix": source_matrix(ROOT),
|
||||
"runs": inventory,
|
||||
"layer_timings_seconds": timings,
|
||||
}
|
||||
out = Path(args.out)
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
out.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
print(f"wrote {out}", flush=True)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,88 @@
|
||||
"""Time local consultation layers on the real workflow path. No VedAstro network."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path[:0] = [str(ROOT), str(ROOT / "scripts"), str(ROOT / "scripts" / "research")]
|
||||
|
||||
from consult_evidence_card_lib import QUESTIONS, load_public_charts # noqa: E402
|
||||
import consult_evidence_card_run as runner # noqa: E402
|
||||
import jyotish_api_server as api # noqa: E402
|
||||
|
||||
|
||||
def _wrap(bucket: dict[str, float], name: str, fn):
|
||||
def inner(*args, **kwargs):
|
||||
started = time.perf_counter()
|
||||
try:
|
||||
return fn(*args, **kwargs)
|
||||
finally:
|
||||
bucket[name] = round(bucket.get(name, 0.0) + time.perf_counter() - started, 4)
|
||||
return inner
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if os.environ.get("PYTHONHASHSEED") != "0":
|
||||
raise SystemExit("Set PYTHONHASHSEED=0")
|
||||
runner._block_external_vedastro()
|
||||
charts = load_public_charts(ROOT)
|
||||
question = QUESTIONS[0]
|
||||
results = {}
|
||||
originals = {
|
||||
"compute_chart": api.JyotishAPIHandler._compute_chart,
|
||||
"thematic": api.JyotishAPIHandler._compute_thematic_report,
|
||||
"rectification": api.JyotishAPIHandler._compute_rectification_gate,
|
||||
"attach": api._attach_local_consultation_layers,
|
||||
"varga_spectrum": api._build_consultation_varga_spectrum,
|
||||
"research_chart": api._custom_research_dn_chart,
|
||||
"varga_full": api.JyotishAPIHandler._compute_varga_full,
|
||||
}
|
||||
for chart in charts:
|
||||
bucket: dict[str, float] = {}
|
||||
calls = {"research_chart": 0}
|
||||
|
||||
def counting_research(*args, **kwargs):
|
||||
calls["research_chart"] += 1
|
||||
return originals["research_chart"](*args, **kwargs)
|
||||
|
||||
api.JyotishAPIHandler._compute_chart = _wrap(bucket, "compute_chart", originals["compute_chart"])
|
||||
api.JyotishAPIHandler._compute_thematic_report = _wrap(bucket, "thematic_report", originals["thematic"])
|
||||
api.JyotishAPIHandler._compute_rectification_gate = _wrap(bucket, "rectification_gate", originals["rectification"])
|
||||
api._attach_local_consultation_layers = _wrap(bucket, "attach_local_layers", originals["attach"])
|
||||
api._build_consultation_varga_spectrum = _wrap(bucket, "varga_spectrum", originals["varga_spectrum"])
|
||||
api._custom_research_dn_chart = _wrap(bucket, "research_dn_loop", counting_research)
|
||||
api.JyotishAPIHandler._compute_varga_full = _wrap(bucket, "varga_full", originals["varga_full"])
|
||||
started = time.perf_counter()
|
||||
runner._run_workflow(runner._workflow_body(chart, question))
|
||||
bucket["workflow_wall"] = round(time.perf_counter() - started, 4)
|
||||
bucket["research_dn_calls"] = calls["research_chart"]
|
||||
results[chart["id"]] = bucket
|
||||
print(chart["id"], bucket, flush=True)
|
||||
for name, fn in originals.items():
|
||||
if name == "compute_chart":
|
||||
api.JyotishAPIHandler._compute_chart = fn
|
||||
elif name == "thematic":
|
||||
api.JyotishAPIHandler._compute_thematic_report = fn
|
||||
elif name == "rectification":
|
||||
api.JyotishAPIHandler._compute_rectification_gate = fn
|
||||
elif name == "attach":
|
||||
api._attach_local_consultation_layers = fn
|
||||
elif name == "varga_spectrum":
|
||||
api._build_consultation_varga_spectrum = fn
|
||||
elif name == "research_chart":
|
||||
api._custom_research_dn_chart = fn
|
||||
elif name == "varga_full":
|
||||
api.JyotishAPIHandler._compute_varga_full = fn
|
||||
out = ROOT / "docs" / "research" / "consult_evidence_card_timings_2026_09_27.json"
|
||||
out.write_text(json.dumps(results, indent=2), encoding="utf-8")
|
||||
print(f"wrote {out}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* Read-only projection of a consultation workflow payload into the text the
|
||||
* answer model sees. Does not modify frontend runtime files.
|
||||
*
|
||||
* The single-domain wrap matches consultation-tools.ts toModelDomainPlanContext
|
||||
* when consultations.length === 1 and omittedDomains.length === 0:
|
||||
* return { ...consultations[0], ...plan, consultations };
|
||||
*/
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { createConsultationPlan } from "../../frontend/src/lib/consultation-plan.ts";
|
||||
import { consultationMethodologyForDomains } from "../../frontend/src/lib/consultation-methodology.ts";
|
||||
import type { ConsultationDomain } from "../../frontend/src/lib/consultation-domain-registry.ts";
|
||||
import {
|
||||
consultationWorkflowResponseSchema,
|
||||
toAgentConsultationContext,
|
||||
toModelOutput,
|
||||
} from "../../frontend/src/mastra/consultation-workflow.ts";
|
||||
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
|
||||
function readJson(path: string): JsonRecord {
|
||||
return JSON.parse(readFileSync(path, "utf8")) as JsonRecord;
|
||||
}
|
||||
|
||||
function planFields(domain: ConsultationDomain, question: string) {
|
||||
const plan = createConsultationPlan({
|
||||
userIntent: question,
|
||||
theme: domain,
|
||||
consultationMode: "verified_chart",
|
||||
});
|
||||
return {
|
||||
plan_version: plan.plan_version,
|
||||
depth: plan.depth,
|
||||
requested_domains: plan.requestedDomains,
|
||||
timing_horizon: plan.timingHorizon,
|
||||
precision_boundary: plan.precisionBoundary,
|
||||
required_evidence_categories: plan.requiredEvidenceCategories,
|
||||
birthTimeMode: plan.birthTimeMode,
|
||||
};
|
||||
}
|
||||
|
||||
function projectOne(workflow: JsonRecord, domain: ConsultationDomain, question: string) {
|
||||
const parsed = consultationWorkflowResponseSchema.safeParse(workflow);
|
||||
const agentContext = toAgentConsultationContext(parsed.success ? parsed.data : workflow);
|
||||
const consultationPlan = createConsultationPlan({
|
||||
userIntent: question,
|
||||
theme: domain,
|
||||
consultationMode: "verified_chart",
|
||||
});
|
||||
const modelOutput = toModelOutput(agentContext, consultationPlan);
|
||||
const consultation = { domain, ...modelOutput };
|
||||
const methodology = consultationMethodologyForDomains([domain]);
|
||||
const plan = {
|
||||
success: agentContext.success === true,
|
||||
domains: [domain],
|
||||
omitted_domains: [] as ConsultationDomain[],
|
||||
...(methodology ? { methodology } : {}),
|
||||
};
|
||||
const modelContext = { ...consultation, ...plan, consultations: [consultation] };
|
||||
return {
|
||||
schema_ok: parsed.success,
|
||||
schema_error: parsed.success ? null : parsed.error.issues.slice(0, 8).map((issue) => ({
|
||||
path: issue.path.join("."),
|
||||
code: issue.code,
|
||||
})),
|
||||
methodology_sections: methodology?.sections.length ?? 0,
|
||||
methodology_chars: methodology
|
||||
? JSON.stringify(methodology).length
|
||||
: 0,
|
||||
model_context: modelContext,
|
||||
};
|
||||
}
|
||||
|
||||
const [mode, ...rest] = process.argv.slice(2);
|
||||
|
||||
if (mode === "plans") {
|
||||
const spec = readJson(rest[0]);
|
||||
const questions = spec.questions as Array<{ id: string; domain: ConsultationDomain; question: string }>;
|
||||
const plans: JsonRecord = {};
|
||||
for (const item of questions) {
|
||||
plans[item.id] = planFields(item.domain, item.question);
|
||||
}
|
||||
writeFileSync(rest[1], JSON.stringify(plans));
|
||||
} else if (mode === "project") {
|
||||
const jobs = readJson(rest[0]) as {
|
||||
items: Array<{ id: string; domain: ConsultationDomain; question: string; workflow_path: string; out_path: string }>;
|
||||
};
|
||||
const summary: JsonRecord[] = [];
|
||||
for (const item of jobs.items) {
|
||||
const workflow = readJson(item.workflow_path);
|
||||
const projected = projectOne(workflow, item.domain, item.question);
|
||||
const text = JSON.stringify(projected.model_context);
|
||||
writeFileSync(item.out_path, text);
|
||||
summary.push({
|
||||
id: item.id,
|
||||
schema_ok: projected.schema_ok,
|
||||
schema_error: projected.schema_error,
|
||||
methodology_sections: projected.methodology_sections,
|
||||
methodology_chars: projected.methodology_chars,
|
||||
model_chars: text.length,
|
||||
});
|
||||
}
|
||||
writeFileSync(rest[1], JSON.stringify(summary));
|
||||
} else {
|
||||
process.stderr.write("usage: plans <spec.json> <out.json> | project <jobs.json> <summary.json>\n");
|
||||
process.exit(2);
|
||||
}
|
||||
Reference in New Issue
Block a user