184 lines
7.4 KiB
Python
184 lines
7.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Validate the commercial astrology acceptance matrix.
|
|
|
|
Default mode is deliberately blocked: the contract exists, but real E2E proof
|
|
requires captured answer-context JSON files for each question id.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
DEFAULT_CONTRACT = ROOT / "references" / "cross_project_contract" / "commercial_astrology_e2e_acceptance_questions_2026_07_19.json"
|
|
|
|
LAYER_PATTERNS: dict[str, list[str]] = {
|
|
"Vimshottari Dasha": ["vimshottari", "dasha"],
|
|
"Narayana Dasha": ["narayana"],
|
|
"D9": ["d9"],
|
|
"D10": ["d10"],
|
|
"D2": ["d2"],
|
|
"D11": ["d11"],
|
|
"D4": ["d4"],
|
|
"D5": ["d5"],
|
|
"D6": ["d6"],
|
|
"D7": ["d7"],
|
|
"D8": ["d8"],
|
|
"D12": ["d12"],
|
|
"D24": ["d24"],
|
|
"UL": ["ul", "upapada"],
|
|
"A10": ["a10"],
|
|
"Darakaraka": ["darakaraka", "dk"],
|
|
"Shadbala": ["shadbala"],
|
|
"Ashtakavarga": ["ashtakavarga"],
|
|
"Shadbala/AV provenance registry": ["shadbala", "ashtakavarga", "provenance"],
|
|
"VedAstro gateway boundary": ["vedastro"],
|
|
"timing precision contract": ["verified_window", "candidate_windows", "exact_triggers"],
|
|
"gender interpretation boundary": ["gender", "spouse", "supplement"],
|
|
"functional benefic/malefic": ["functional_benefic_malefic"],
|
|
"12th house": ["12th", "twelfth", "house_12"],
|
|
"6th/8th/12th house": ["6th", "8th", "12th"],
|
|
"4th/5th/9th house": ["4th", "5th", "9th"],
|
|
"5th/9th house": ["5th", "9th"],
|
|
"Rahu/Ketu context": ["rahu", "ketu"],
|
|
"Mercury/Jupiter context": ["mercury", "jupiter"],
|
|
"Ashtakoot boundary": ["ashtakoot"],
|
|
"birth-time uncertainty boundary": ["birth_time", "uncertain"],
|
|
"rectification boundary": ["rectification"],
|
|
"candidate windows": ["candidate_windows"],
|
|
"not_auto_rectified": ["not_auto_rectified"],
|
|
"no_majority_vote": ["no_majority_vote"],
|
|
"verified_window": ["verified_window"],
|
|
"candidate_windows": ["candidate_windows"],
|
|
"exact_triggers": ["exact_triggers"],
|
|
"day-level holdout boundary": ["holdout", "exploratory_unvalidated"],
|
|
"health non-medical boundary": ["health", "non_medical"],
|
|
"annual forecast boundary": ["annual", "forecast"],
|
|
"transit context": ["transit"],
|
|
}
|
|
|
|
FORBIDDEN_PATTERNS: dict[str, list[str]] = {
|
|
"exact day/month verified": ["exact_day_verified", "exact_month_verified"],
|
|
"single-factor Venus/Jupiter truth": ["single_factor_venus_jupiter_truth"],
|
|
"gender changes career math": ["gender_changes_career_math"],
|
|
"D10 missing when present": ["d10_missing_when_present"],
|
|
"guaranteed financial outcome": ["guaranteed_financial_outcome"],
|
|
"D2/D11 missing when present": ["d2_d11_missing_when_present"],
|
|
"medical diagnosis": ["medical_diagnosis"],
|
|
"guaranteed disease event": ["guaranteed_disease_event"],
|
|
"visa/legal guarantee": ["visa_guarantee", "legal_guarantee"],
|
|
"guaranteed child outcome": ["guaranteed_child_outcome"],
|
|
"property/legal guarantee": ["property_guarantee", "legal_guarantee"],
|
|
"exam result guarantee": ["exam_result_guarantee"],
|
|
"single-factor Mercury/Jupiter truth": ["single_factor_mercury_jupiter_truth"],
|
|
"relationship guarantee": ["relationship_guarantee"],
|
|
"gender binary forced when not provided": ["forced_binary_gender"],
|
|
"auto-rectified exact birth time": ["auto_rectified_exact_birth_time"],
|
|
"majority vote truth": ["majority_vote_truth"],
|
|
"method variant as absolute error": ["method_variant_absolute_error"],
|
|
"day/month prediction verified before holdout": ["day_month_verified_before_holdout"],
|
|
"exact_triggers as guaranteed events": ["exact_triggers_guaranteed_events"],
|
|
"full-year certainty": ["full_year_certainty"],
|
|
}
|
|
|
|
|
|
def _flatten(value: Any) -> str:
|
|
return json.dumps(value, ensure_ascii=False, sort_keys=True).lower()
|
|
|
|
|
|
def _has_layer(text: str, layer: str) -> bool:
|
|
patterns = LAYER_PATTERNS.get(layer, [layer.lower()])
|
|
return all(pattern.lower() in text for pattern in patterns)
|
|
|
|
|
|
def _has_forbidden(text: str, claim: str) -> bool:
|
|
return any(pattern.lower() in text for pattern in FORBIDDEN_PATTERNS.get(claim, [claim.lower()]))
|
|
|
|
|
|
def _load_json(path: Path) -> Any:
|
|
return json.loads(path.read_text(encoding="utf-8"))
|
|
|
|
|
|
def evaluate(contract_path: Path = DEFAULT_CONTRACT, context_dir: Path | None = None) -> dict[str, Any]:
|
|
contract = _load_json(contract_path)
|
|
rows: list[dict[str, Any]] = []
|
|
status = "pass"
|
|
|
|
for question in contract["questions"]:
|
|
qid = question["id"]
|
|
context_path = context_dir / f"{qid}.json" if context_dir else None
|
|
if not context_path or not context_path.exists():
|
|
rows.append(
|
|
{
|
|
"id": qid,
|
|
"status": "blocked",
|
|
"reason": "missing_runtime_context_json",
|
|
"required_layers": question["required_layers"],
|
|
}
|
|
)
|
|
status = "blocked"
|
|
continue
|
|
|
|
text = _flatten(_load_json(context_path))
|
|
missing = [layer for layer in question["required_layers"] if not _has_layer(text, layer)]
|
|
forbidden = [claim for claim in question["must_not_claim"] if _has_forbidden(text, claim)]
|
|
row_status = "pass" if not missing and not forbidden else "fail"
|
|
if row_status == "fail":
|
|
status = "fail"
|
|
rows.append({"id": qid, "status": row_status, "missing_layers": missing, "forbidden_claim_hits": forbidden})
|
|
|
|
return {
|
|
"scope": "commercial_astrology_e2e_acceptance_runner",
|
|
"contract": str(contract_path.relative_to(ROOT) if contract_path.is_relative_to(ROOT) else contract_path),
|
|
"status": status,
|
|
"runtime_context_required": True,
|
|
"question_count": len(contract["questions"]),
|
|
"rows": rows,
|
|
}
|
|
|
|
|
|
def write_question_manifest(contract_path: Path, output_path: Path) -> dict[str, Any]:
|
|
contract = _load_json(contract_path)
|
|
manifest = {
|
|
"scope": "commercial_astrology_e2e_question_manifest",
|
|
"contract_id": contract["contract_id"],
|
|
"capture_instruction": "Run each user_question through the commercial consultation flow and save the answer context as <id>.json for this runner.",
|
|
"questions": [
|
|
{
|
|
"id": question["id"],
|
|
"user_question": question["user_question"],
|
|
"required_layers": question["required_layers"],
|
|
"context_filename": f"{question['id']}.json",
|
|
}
|
|
for question in contract["questions"]
|
|
],
|
|
}
|
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
output_path.write_text(json.dumps(manifest, ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
return manifest
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--contract", type=Path, default=DEFAULT_CONTRACT)
|
|
parser.add_argument("--context-dir", type=Path)
|
|
parser.add_argument("--write-question-manifest", type=Path)
|
|
args = parser.parse_args()
|
|
|
|
if args.write_question_manifest:
|
|
manifest = write_question_manifest(args.contract, args.write_question_manifest)
|
|
print(json.dumps(manifest, ensure_ascii=False, indent=2, sort_keys=True))
|
|
return 0
|
|
|
|
report = evaluate(args.contract, args.context_dir)
|
|
print(json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True))
|
|
return 0 if report["status"] == "pass" else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|