把星盘接口的计算和 HTTP 拆成两个类,离线入口不再伪造请求对象。
方法体未改。定向测试 383 通过;1 条校正回归在基线上同样失败,断言未改。快速门和全量 pytest 未跑。 Co-Authored-By: Grok (agent loop)
This commit is contained in:
co-authored by
Grok (agent loop)
parent
fbf04f0b5f
commit
c7350bcafb
@@ -13,7 +13,7 @@ ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "scripts"))
|
||||
|
||||
from consultation_plan_contract import PLAN_VERSION, _ROUTE_CONTRACTS # noqa: E402
|
||||
from jyotish_api_server import JyotishAPIHandler # noqa: E402
|
||||
from jyotish_api_server import JyotishComputeService # noqa: E402
|
||||
|
||||
BIRTH = {
|
||||
"year": 1993,
|
||||
@@ -55,8 +55,8 @@ CONSUMER_KEEP = (
|
||||
CHART_KEEP = ("success", "ascendant", "planets", "houses", "dasha")
|
||||
|
||||
|
||||
def _handler() -> JyotishAPIHandler:
|
||||
return JyotishAPIHandler.__new__(JyotishAPIHandler)
|
||||
def _handler() -> JyotishComputeService:
|
||||
return JyotishComputeService()
|
||||
|
||||
|
||||
def _body(theme: str) -> dict:
|
||||
|
||||
@@ -15,9 +15,9 @@ for path in (ROOT, SCRIPTS_DIR):
|
||||
|
||||
|
||||
def execute_consultation_workflow(body: dict[str, Any], *, surface: str = "api") -> dict[str, Any]:
|
||||
from jyotish_api_server import JyotishAPIHandler, execute_consultation_workflow as _execute
|
||||
from jyotish_api_server import JyotishComputeService, execute_consultation_workflow as _execute
|
||||
|
||||
handler = JyotishAPIHandler.__new__(JyotishAPIHandler)
|
||||
handler = JyotishComputeService()
|
||||
return _execute(handler, body=body, surface=surface)
|
||||
|
||||
|
||||
|
||||
+594
-580
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,273 @@
|
||||
"""Canonical hashes for the compute/HTTP class split.
|
||||
|
||||
Fictional smoke chart only. Clock fields are removed before hashing.
|
||||
Payload values are not written; the golden file stores sha256 and key-path types.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
SCRIPTS = ROOT / "scripts"
|
||||
GOLDEN_PATH = ROOT / "tests" / "golden" / "api_server_service_extract_v1.json"
|
||||
CACHE_DIR = ROOT / "scratch" / "local" / "api_chart_cache"
|
||||
CLOCK_KEYS = {"cache_created_at", "cache_expires_at"}
|
||||
THEMES = ("career", "wealth", "relationship")
|
||||
BASELINE_SHA = "fbf04f0b5f31cd8421bc4546840b1c833aaed810"
|
||||
|
||||
# Synthetic positions reused from tests/test_offline_compute_mixins.py.
|
||||
# Not a birth record.
|
||||
EVIDENCE_CHART = {
|
||||
"success": True,
|
||||
"ascendant": {"sign": "Aries", "sign_idx": 0, "degree": 12.0, "lon": 12.0},
|
||||
"planets": {
|
||||
"Sun": {"sign": "Leo", "lon": 130.0},
|
||||
"Moon": {"sign": "Cancer", "lon": 100.0},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def prepare_env() -> None:
|
||||
os.environ["VEDASTRO_ENABLE_NETWORK"] = "0"
|
||||
os.environ.setdefault("JYOTISH_SKIP_LOCAL_ENV", "1")
|
||||
os.environ["PYTHONDONTWRITEBYTECODE"] = "1"
|
||||
for path in (str(ROOT), str(SCRIPTS)):
|
||||
if path not in sys.path:
|
||||
sys.path.insert(0, path)
|
||||
|
||||
|
||||
def clear_chart_cache() -> None:
|
||||
if CACHE_DIR.exists():
|
||||
shutil.rmtree(CACHE_DIR)
|
||||
|
||||
|
||||
def theme_body(theme: str) -> dict[str, Any]:
|
||||
"""Fictional smoke chart from the task brief. Coordinates only, no person."""
|
||||
return {
|
||||
"year": 1990,
|
||||
"month": 1,
|
||||
"day": 1,
|
||||
"hour": 12,
|
||||
"minute": 0,
|
||||
"second": 0,
|
||||
"lat": 28.6139,
|
||||
"lon": 77.2090,
|
||||
"tz": 5.5,
|
||||
"theme": [theme],
|
||||
"defer_optional_external_evidence": True,
|
||||
"today": "2026-10-13",
|
||||
}
|
||||
|
||||
|
||||
def strip_clock(value: Any) -> Any:
|
||||
if isinstance(value, dict):
|
||||
return {
|
||||
key: strip_clock(item)
|
||||
for key, item in value.items()
|
||||
if key not in CLOCK_KEYS
|
||||
}
|
||||
if isinstance(value, list):
|
||||
return [strip_clock(item) for item in value]
|
||||
return value
|
||||
|
||||
|
||||
def json_type(value: Any) -> str:
|
||||
if value is None:
|
||||
return "null"
|
||||
if isinstance(value, bool):
|
||||
return "boolean"
|
||||
if isinstance(value, (int, float)):
|
||||
return "number"
|
||||
if isinstance(value, str):
|
||||
return "string"
|
||||
if isinstance(value, list):
|
||||
return "array"
|
||||
if isinstance(value, dict):
|
||||
return "object"
|
||||
return type(value).__name__
|
||||
|
||||
|
||||
def flatten_keypaths(value: Any, prefix: str = "", *, depth: int = 0, max_depth: int = 2) -> dict[str, str]:
|
||||
out: dict[str, str] = {}
|
||||
label = prefix or "$"
|
||||
out[label] = json_type(value)
|
||||
if depth >= max_depth:
|
||||
return out
|
||||
if isinstance(value, dict):
|
||||
for key in sorted(item for item in value if isinstance(item, str)):
|
||||
child = f"{prefix}.{key}" if prefix else key
|
||||
out.update(flatten_keypaths(value[key], child, depth=depth + 1, max_depth=max_depth))
|
||||
elif isinstance(value, list) and value:
|
||||
child = f"{prefix}[]" if prefix else "[]"
|
||||
out.update(flatten_keypaths(value[0], child, depth=depth + 1, max_depth=max_depth))
|
||||
return out
|
||||
|
||||
|
||||
def canonical_text(value: Any) -> str:
|
||||
return json.dumps(strip_clock(value), sort_keys=True, ensure_ascii=False)
|
||||
|
||||
|
||||
def digest(value: Any) -> dict[str, Any]:
|
||||
text = canonical_text(value)
|
||||
return {
|
||||
"sha256": hashlib.sha256(text.encode("utf-8")).hexdigest(),
|
||||
"canonicalBytes": len(text.encode("utf-8")),
|
||||
"keypaths": flatten_keypaths(strip_clock(value)),
|
||||
}
|
||||
|
||||
|
||||
def capture_mcp(theme: str) -> Any:
|
||||
prepare_env()
|
||||
clear_chart_cache()
|
||||
from consultation_workflow_service import execute_consultation_workflow
|
||||
|
||||
return execute_consultation_workflow(theme_body(theme), surface="skill_mcp")
|
||||
|
||||
|
||||
def capture_http(theme: str) -> Any:
|
||||
"""HTTP handler method, on the compute class after the split."""
|
||||
prepare_env()
|
||||
clear_chart_cache()
|
||||
from jyotish_api_server import JyotishComputeService
|
||||
|
||||
return JyotishComputeService()._compute_consultation_workflow(theme_body(theme))
|
||||
|
||||
|
||||
def capture_repairs() -> dict[str, Any]:
|
||||
prepare_env()
|
||||
clear_chart_cache()
|
||||
from capture_report_blocked_repairs_golden import (
|
||||
BIRTH,
|
||||
MODULE_KEEP,
|
||||
THEMES as REPAIR_THEMES,
|
||||
VARGA_KEEP,
|
||||
_body,
|
||||
_handler,
|
||||
_trim_workflow,
|
||||
)
|
||||
|
||||
payload: dict[str, Any] = {
|
||||
"source": "scripts/jyotish_api_server.py _compute_consultation_workflow",
|
||||
"smoke_birth": {**BIRTH, "note": "fictional capture identity; not a real person"},
|
||||
"trim": {
|
||||
"keep_varga_keys": list(VARGA_KEEP),
|
||||
"keep_modules": list(MODULE_KEEP),
|
||||
"drop": [
|
||||
"ai_prompt_pack",
|
||||
"vedastro",
|
||||
"varga_spectrum",
|
||||
"guided_topics",
|
||||
"western_spectrum",
|
||||
"technique_audit_table",
|
||||
],
|
||||
},
|
||||
"themes": {},
|
||||
}
|
||||
for theme in REPAIR_THEMES:
|
||||
clear_chart_cache()
|
||||
raw = _handler()._compute_consultation_workflow(_body(theme))
|
||||
payload["themes"][theme] = _trim_workflow(raw)
|
||||
return payload
|
||||
|
||||
|
||||
def capture_evidence() -> Any:
|
||||
prepare_env()
|
||||
from consultation_workflow_service import build_runtime_evidence_helpers
|
||||
|
||||
return build_runtime_evidence_helpers(EVIDENCE_CHART)
|
||||
|
||||
|
||||
def capture_cases(*, include_http: bool = True, include_repairs: bool = True) -> dict[str, Any]:
|
||||
cases: dict[str, Any] = {}
|
||||
for theme in THEMES:
|
||||
cases[f"mcp.{theme}"] = capture_mcp(theme)
|
||||
if include_http:
|
||||
cases[f"http.{theme}"] = capture_http(theme)
|
||||
if include_repairs:
|
||||
cases["repairs"] = capture_repairs()
|
||||
cases["evidence"] = capture_evidence()
|
||||
return cases
|
||||
|
||||
|
||||
def summarize(cases: dict[str, Any]) -> dict[str, Any]:
|
||||
return {name: digest(value) for name, value in cases.items()}
|
||||
|
||||
|
||||
def provenance() -> dict[str, Any]:
|
||||
return {
|
||||
"fictional": True,
|
||||
"command": "python scripts/research/api_service_extract_golden.py --check",
|
||||
"baselineSha": BASELINE_SHA,
|
||||
"env": {
|
||||
"VEDASTRO_ENABLE_NETWORK": "0",
|
||||
"JYOTISH_SKIP_LOCAL_ENV": "1",
|
||||
},
|
||||
"clockFieldsRemoved": sorted(CLOCK_KEYS),
|
||||
"mcpSurface": "skill_mcp",
|
||||
"note": "fictional smoke chart from the task brief; payload values are not stored",
|
||||
}
|
||||
|
||||
|
||||
def document_from_summary(summary: dict[str, Any]) -> dict[str, Any]:
|
||||
return {
|
||||
"kind": "sha256_and_keypath_types",
|
||||
"fixtureProvenance": provenance(),
|
||||
"cases": summary,
|
||||
}
|
||||
|
||||
|
||||
def write_golden(summary: dict[str, Any], path: Path = GOLDEN_PATH) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
text = json.dumps(document_from_summary(summary), ensure_ascii=False, indent=2, sort_keys=True) + "\n"
|
||||
path.write_text(text, encoding="utf-8")
|
||||
|
||||
|
||||
def load_golden(path: Path = GOLDEN_PATH) -> dict[str, Any]:
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def check_golden(path: Path = GOLDEN_PATH) -> list[str]:
|
||||
expected = load_golden(path)["cases"]
|
||||
actual = summarize(capture_cases())
|
||||
mismatches: list[str] = []
|
||||
if set(expected) != set(actual):
|
||||
mismatches.append(f"case names {sorted(expected)} != {sorted(actual)}")
|
||||
for name in sorted(set(expected) & set(actual)):
|
||||
if expected[name]["sha256"] != actual[name]["sha256"]:
|
||||
mismatches.append(
|
||||
f"{name} sha256 {expected[name]['sha256']} != {actual[name]['sha256']} "
|
||||
f"bytes {expected[name]['canonicalBytes']} != {actual[name]['canonicalBytes']}"
|
||||
)
|
||||
elif expected[name]["keypaths"] != actual[name]["keypaths"]:
|
||||
mismatches.append(f"{name} keypaths differ")
|
||||
return mismatches
|
||||
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
prepare_env()
|
||||
if "--check" in argv:
|
||||
mismatches = check_golden()
|
||||
if mismatches:
|
||||
print("MISMATCH")
|
||||
for item in mismatches:
|
||||
print(item)
|
||||
return 1
|
||||
print("MATCH", len(load_golden()["cases"]))
|
||||
return 0
|
||||
summary = summarize(capture_cases())
|
||||
for name, row in summary.items():
|
||||
print(f"{name} {row['sha256']} {row['canonicalBytes']}")
|
||||
if "--write" in argv:
|
||||
write_golden(summary)
|
||||
print("WROTE", GOLDEN_PATH)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main(sys.argv[1:]))
|
||||
Reference in New Issue
Block a user