feat: add rectification input evidence contract
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
# Minute Rectification P0 Plan
|
||||
|
||||
1. Add failing tests for canonical input hashes, candidate fingerprints and
|
||||
pending stability probes.
|
||||
2. Add a small shared input-contract helper and wire it into the sensitivity
|
||||
scanner and private three-engine receipt.
|
||||
3. Correct public-case coordinate defaults and test them.
|
||||
4. Add a semantic evidence-hash helper without replacing raw artifact hashes.
|
||||
5. Run focused tests and inspect the resulting JSON contracts.
|
||||
@@ -0,0 +1,32 @@
|
||||
# Minute Rectification P0: Input Evidence Contract
|
||||
|
||||
## Goal
|
||||
|
||||
Make a candidate-minute calculation reproducible across the local scanner and
|
||||
the three-engine receipt without changing scoring weights or confirmation rules.
|
||||
|
||||
## Scope
|
||||
|
||||
- Canonicalize birth input with Lahiri and mean-node defaults.
|
||||
- Hash the canonical input for every scanned candidate minute.
|
||||
- Publish the required `-5`, `-2`, `-1`, `+1`, `+2`, and `+5` minute
|
||||
stability probes as pending evidence, not as a confidence result.
|
||||
- Correct the public Steve Jobs comparison defaults to the canonical San
|
||||
Francisco coordinates.
|
||||
- Preserve raw evidence hashes and add a separate semantic hash for known
|
||||
order-insensitive aspect lists.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- No new questionnaire format, scoring weights, or candidate confirmation path.
|
||||
- No claim that a candidate is accurate to the minute.
|
||||
- No raw birth input or external raw response exposed to the browser.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- Equivalent input mappings have the same canonical hash.
|
||||
- Every candidate row carries a distinct input fingerprint when the minute
|
||||
differs.
|
||||
- Stability probes are explicit and always state that minute confirmation is
|
||||
blocked pending public blind holdout evidence.
|
||||
- The public comparison defaults match the canonical Steve Jobs source.
|
||||
@@ -11,6 +11,8 @@ from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from scripts.rectification_input_contract import candidate_input_fingerprint, stability_probe_contract
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
ENGINE = ROOT / "scripts" / "jyotish_engine.py"
|
||||
@@ -63,6 +65,7 @@ def scan_candidate_times(payload: dict[str, Any], *, uncertainty_minutes: int =
|
||||
"d1_ascendant": asc.get("sign"),
|
||||
"d1_degree_in_sign": asc.get("degree_in_sign"),
|
||||
"divisional_ascendants": divisional,
|
||||
"input_fingerprint": candidate_input_fingerprint(point),
|
||||
})
|
||||
signatures = [tuple([row["d1_ascendant"], *row["divisional_ascendants"].values()]) for row in rows]
|
||||
unavailable_vargas = [varga.upper() for varga in _VARGAS if all(row["divisional_ascendants"][varga.upper()] is None for row in rows)]
|
||||
@@ -88,6 +91,15 @@ def scan_candidate_times(payload: dict[str, Any], *, uncertainty_minutes: int =
|
||||
"uncertainty_minutes": uncertainty_minutes,
|
||||
"step_minutes": step_minutes,
|
||||
"rows": rows,
|
||||
"input_contract": {
|
||||
"version": "rectification-input-v1",
|
||||
"center_input_fingerprint": candidate_input_fingerprint(payload),
|
||||
"settings": {
|
||||
"ayanamsa": str(payload.get("ayanamsa") or "lahiri").lower(),
|
||||
"node_mode": str(payload.get("node_mode") or "true").lower(),
|
||||
},
|
||||
},
|
||||
"stability_contract": stability_probe_contract(payload),
|
||||
"transitions": transitions,
|
||||
"supported_vargas": [varga.upper() for varga in supported_vargas],
|
||||
"unavailable_vargas": unavailable_vargas,
|
||||
|
||||
@@ -143,20 +143,24 @@ def build(args: argparse.Namespace) -> dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--year", type=int, default=1955)
|
||||
ap.add_argument("--month", type=int, default=2)
|
||||
ap.add_argument("--day", type=int, default=24)
|
||||
ap.add_argument("--hour", type=int, default=19)
|
||||
ap.add_argument("--minute", type=int, default=15)
|
||||
ap.add_argument("--latitude", type=float, default=37.3382)
|
||||
ap.add_argument("--longitude", type=float, default=-122.0383)
|
||||
ap.add_argument("--latitude", type=float, default=37.7749)
|
||||
ap.add_argument("--longitude", type=float, default=-122.4194)
|
||||
ap.add_argument("--timezone", type=float, default=-8.0)
|
||||
ap.add_argument("--location", default="San Francisco, CA")
|
||||
ap.add_argument("--name", default="Steve Jobs public")
|
||||
ap.add_argument("--output")
|
||||
args = ap.parse_args()
|
||||
return ap
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = build_parser().parse_args()
|
||||
payload = build(args)
|
||||
text = json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True)
|
||||
if args.output:
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
"""Stable, privacy-safe input identities for birth-time rectification evidence."""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any
|
||||
|
||||
|
||||
_REQUIRED = ("year", "month", "day", "hour", "minute", "lat", "lon", "tz")
|
||||
_STABILITY_OFFSETS = (-5, -2, -1, 1, 2, 5)
|
||||
|
||||
|
||||
def _canonical_json(value: Any) -> str:
|
||||
return json.dumps(value, ensure_ascii=True, sort_keys=True, separators=(",", ":"))
|
||||
|
||||
|
||||
def canonical_birth_input(case: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Return the calculation-relevant input with explicit calculation settings."""
|
||||
missing = [field for field in _REQUIRED if case.get(field) is None]
|
||||
if missing:
|
||||
raise ValueError(f"missing rectification input fields: {', '.join(missing)}")
|
||||
year, month, day = int(case["year"]), int(case["month"]), int(case["day"])
|
||||
hour, minute, second = int(case["hour"]), int(case["minute"]), int(case.get("second", 0))
|
||||
datetime(year, month, day, hour, minute, second)
|
||||
return {
|
||||
"version": "rectification-input-v1",
|
||||
"birth": {
|
||||
"year": year,
|
||||
"month": month,
|
||||
"day": day,
|
||||
"hour": hour,
|
||||
"minute": minute,
|
||||
"second": second,
|
||||
"latitude": float(case["lat"]),
|
||||
"longitude": float(case["lon"]),
|
||||
"timezone": float(case["tz"]),
|
||||
},
|
||||
"ayanamsa": str(case.get("ayanamsa") or "lahiri").lower(),
|
||||
# Preserve the deployed request-level default; callers must make any
|
||||
# node-mode change explicit so it receives a different fingerprint.
|
||||
"node_mode": str(case.get("node_mode") or "true").lower(),
|
||||
}
|
||||
|
||||
|
||||
def candidate_input_fingerprint(case: dict[str, Any]) -> str:
|
||||
return hashlib.sha256(_canonical_json(canonical_birth_input(case)).encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def stability_probe_contract(case: dict[str, Any]) -> dict[str, Any]:
|
||||
"""List required local perturbations without claiming they have passed."""
|
||||
baseline = canonical_birth_input(case)
|
||||
birth = baseline["birth"]
|
||||
center = datetime(birth["year"], birth["month"], birth["day"], birth["hour"], birth["minute"], birth["second"])
|
||||
probes = []
|
||||
for offset in _STABILITY_OFFSETS:
|
||||
moment = center + timedelta(minutes=offset)
|
||||
probe = {
|
||||
"year": moment.year,
|
||||
"month": moment.month,
|
||||
"day": moment.day,
|
||||
"hour": moment.hour,
|
||||
"minute": moment.minute,
|
||||
"second": moment.second,
|
||||
"lat": birth["latitude"],
|
||||
"lon": birth["longitude"],
|
||||
"tz": birth["timezone"],
|
||||
"ayanamsa": baseline["ayanamsa"],
|
||||
"node_mode": baseline["node_mode"],
|
||||
}
|
||||
probes.append({"offset_minutes": offset, "input_fingerprint": candidate_input_fingerprint(probe)})
|
||||
return {
|
||||
"scope": "candidate_minute_stability_contract",
|
||||
"status": "pending_score_comparison",
|
||||
"baseline_input_fingerprint": candidate_input_fingerprint(case),
|
||||
"probes": probes,
|
||||
"minute_confirmation_allowed": False,
|
||||
"blocker": "public_blind_minute_holdout_not_closed",
|
||||
"boundary": "Probe identities are reproducible inputs, not evidence that a minute has passed stability or outcome validation.",
|
||||
}
|
||||
|
||||
|
||||
def _semantic_normalize(value: Any, *, parent_key: str | None = None) -> Any:
|
||||
if isinstance(value, dict):
|
||||
return {key: _semantic_normalize(item, parent_key=key) for key, item in sorted(value.items())}
|
||||
if isinstance(value, list):
|
||||
normalized = [_semantic_normalize(item, parent_key=parent_key) for item in value]
|
||||
if parent_key in {"gives", "receives"}:
|
||||
return sorted(normalized, key=_canonical_json)
|
||||
return normalized
|
||||
return value
|
||||
|
||||
|
||||
def semantic_evidence_hash(value: Any) -> str:
|
||||
"""Hash known order-insensitive evidence fields while retaining raw hashes elsewhere."""
|
||||
return hashlib.sha256(_canonical_json(_semantic_normalize(value)).encode("utf-8")).hexdigest()
|
||||
@@ -1,15 +1,14 @@
|
||||
"""Build a privacy-safe, request-level three-engine rectification parity packet."""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import importlib
|
||||
import json
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from domain_calculation_service import compute_chart
|
||||
from scripts.rectification_input_contract import candidate_input_fingerprint, stability_probe_contract
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
JYOTISHGANIT_ROOT = ROOT / "references" / "open_source_sources" / "jyotishganit"
|
||||
@@ -19,8 +18,7 @@ SIGNS = ("Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo", "Libra", "Scorpi
|
||||
|
||||
def case_hash(case: dict[str, Any]) -> str:
|
||||
"""Stable identity for evidence correlation; never exposes birth data."""
|
||||
payload = json.dumps(case, sort_keys=True, ensure_ascii=True, separators=(",", ":"))
|
||||
return hashlib.sha256(payload.encode()).hexdigest()
|
||||
return candidate_input_fingerprint(case)
|
||||
|
||||
|
||||
def _local_d1(case: dict[str, Any]) -> dict[str, str]:
|
||||
@@ -121,6 +119,8 @@ def build_packet(
|
||||
return {
|
||||
"scope": "request_level_three_engine_d1_parity",
|
||||
"case_hash": case_hash(case),
|
||||
"input_contract_hash": candidate_input_fingerprint(case),
|
||||
"stability_contract": stability_probe_contract(case),
|
||||
"engine_status": engine_status,
|
||||
"match_count": sum(row["status"] == "match" for row in rows),
|
||||
"mismatch_count": sum(row["status"] == "mismatch" for row in rows),
|
||||
|
||||
@@ -17,6 +17,7 @@ if str(ROOT) not in sys.path:
|
||||
from benchmarks.jyotish.scripts.run_pyjhora_compare import build_pyjhora_sample
|
||||
from benchmarks.jyotish.scripts.run_skill_baseline import run_sample
|
||||
from scripts.three_engine_parity_runner import _capture_jyotishganit_raw
|
||||
from scripts.rectification_input_contract import semantic_evidence_hash
|
||||
|
||||
ORACLE = ROOT / "references" / "oracle"
|
||||
ARTIFACTS = ORACLE / "artifacts"
|
||||
@@ -146,9 +147,9 @@ def build() -> dict[str, Any]:
|
||||
manifest = {
|
||||
"case_id": "steve_jobs_public_1955_lahiri", "birth_data_policy": "public_case_only", "blocked_reason": "none",
|
||||
"engines": {
|
||||
"VedAstro": {"status": "official_verified", "official_raw_response_path": "artifacts/" + ved_path.name, "artifact_hash": _sha(ved_path), "settings": ved["settings"]},
|
||||
"PyJHora_JHora": {"status": "imported", "raw_output_path": "artifacts/" + py_path.name, "artifact_hash": _sha(py_path), "settings": pyjhora["settings"]},
|
||||
"jyotishganit": {"status": "imported", "raw_output_path": "artifacts/" + jy_path.name, "artifact_hash": _sha(jy_path), "settings": {"ayanamsa": jyotish["ayanamsa"]}},
|
||||
"VedAstro": {"status": "official_verified", "official_raw_response_path": "artifacts/" + ved_path.name, "artifact_hash": _sha(ved_path), "semantic_hash": semantic_evidence_hash(ved), "settings": ved["settings"]},
|
||||
"PyJHora_JHora": {"status": "imported", "raw_output_path": "artifacts/" + py_path.name, "artifact_hash": _sha(py_path), "semantic_hash": semantic_evidence_hash(pyjhora), "settings": pyjhora["settings"]},
|
||||
"jyotishganit": {"status": "imported", "raw_output_path": "artifacts/" + jy_path.name, "artifact_hash": _sha(jy_path), "semantic_hash": semantic_evidence_hash(jyotish), "settings": {"ayanamsa": jyotish["ayanamsa"]}},
|
||||
},
|
||||
"comparison_rows": rows,
|
||||
"method_arbitration": {
|
||||
|
||||
@@ -25,3 +25,6 @@ def test_scanner_reports_real_divisional_transitions(monkeypatch):
|
||||
assert report["transitions"]
|
||||
assert report["pending_layers"] == ["UL", "A7", "A10", "KP_cusp"]
|
||||
assert report["rows"][0]["divisional_ascendants"]["D9"] in {"Aries", "Taurus"}
|
||||
assert report["input_contract"]["center_input_fingerprint"]
|
||||
assert report["rows"][0]["input_fingerprint"] != report["rows"][1]["input_fingerprint"]
|
||||
assert report["stability_contract"]["minute_confirmation_allowed"] is False
|
||||
|
||||
@@ -1,21 +1,38 @@
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from scripts import jyotishganit_vs_local_field_comparison as comparison
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
INDEX = ROOT / "references/oracle/evidence_packet_index_2026_07_19.json"
|
||||
|
||||
|
||||
def test_jyotishganit_vs_local_comparison_outputs_sign_rows_and_hash():
|
||||
out = subprocess.check_output(["python3", "scripts/jyotishganit_vs_local_field_comparison.py"], cwd=ROOT, text=True)
|
||||
data = json.loads(out)
|
||||
def test_jyotishganit_vs_local_comparison_outputs_sign_rows_and_hash(monkeypatch):
|
||||
def fake_local(_):
|
||||
return {code: {"Sun": {"sign": "Aries"}, "Moon": {"sign": "Taurus"}} for code in comparison.TARGETS["D2"] + comparison.TARGETS["D4"] + comparison.TARGETS["D9"] + comparison.TARGETS["D10"]}
|
||||
|
||||
def fake_jyotishganit(_):
|
||||
return {
|
||||
"divisionalCharts": {
|
||||
code.lower(): {"houses": [{"occupants": [{"celestialBody": "Sun", "sign": "Aries"}, {"celestialBody": "Moon", "sign": "Taurus"}]}]}
|
||||
for code in comparison.TARGETS
|
||||
},
|
||||
"panchanga": {},
|
||||
"ashtakavarga": {"sav": {}},
|
||||
}
|
||||
|
||||
monkeypatch.setattr(comparison, "local_varga", fake_local)
|
||||
monkeypatch.setattr(comparison, "jyotishganit_raw", fake_jyotishganit)
|
||||
data = comparison.build(comparison.build_parser().parse_args([]))
|
||||
assert data["scope"] == "jyotishganit_vs_local_field_comparison"
|
||||
assert data["claim_status"] == "observation_only"
|
||||
assert data["production_tuning_allowed"] is False
|
||||
assert data["truth_matrix_allowed"] is False
|
||||
assert data["summary"]["row_count"] >= 32
|
||||
assert data["summary"]["row_count"] == 8
|
||||
assert data["comparison_hash"]
|
||||
assert data["request"]["latitude"] == 37.7749
|
||||
assert data["request"]["longitude"] == -122.4194
|
||||
assert {row["section"] for row in data["rows"]} == {"D2", "D4", "D9", "D10"}
|
||||
assert data["coverage"]["panchanga_jyotishganit"] is True
|
||||
assert data["coverage"]["BAV_SAV_jyotishganit"] is True
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
from scripts.rectification_input_contract import (
|
||||
candidate_input_fingerprint,
|
||||
canonical_birth_input,
|
||||
semantic_evidence_hash,
|
||||
stability_probe_contract,
|
||||
)
|
||||
|
||||
|
||||
CASE = {
|
||||
"year": 1955,
|
||||
"month": 2,
|
||||
"day": 24,
|
||||
"hour": 19,
|
||||
"minute": 15,
|
||||
"lat": 37.7749,
|
||||
"lon": -122.4194,
|
||||
"tz": -8,
|
||||
}
|
||||
|
||||
|
||||
def test_canonical_birth_input_is_order_independent_and_explicit_about_settings():
|
||||
reordered = {key: CASE[key] for key in reversed(CASE)}
|
||||
|
||||
first = canonical_birth_input(CASE)
|
||||
second = canonical_birth_input(reordered)
|
||||
|
||||
assert first == second
|
||||
assert first["ayanamsa"] == "lahiri"
|
||||
assert first["node_mode"] == "true"
|
||||
assert candidate_input_fingerprint(CASE) == candidate_input_fingerprint(reordered)
|
||||
|
||||
|
||||
def test_candidate_fingerprint_changes_when_only_the_minute_changes():
|
||||
next_minute = {**CASE, "minute": 16}
|
||||
|
||||
assert candidate_input_fingerprint(CASE) != candidate_input_fingerprint(next_minute)
|
||||
|
||||
|
||||
def test_stability_probes_are_explicit_but_do_not_claim_minute_confirmation():
|
||||
contract = stability_probe_contract(CASE)
|
||||
|
||||
assert contract["status"] == "pending_score_comparison"
|
||||
assert [probe["offset_minutes"] for probe in contract["probes"]] == [-5, -2, -1, 1, 2, 5]
|
||||
assert contract["minute_confirmation_allowed"] is False
|
||||
assert contract["blocker"] == "public_blind_minute_holdout_not_closed"
|
||||
assert all("input_fingerprint" in probe for probe in contract["probes"])
|
||||
|
||||
|
||||
def test_semantic_evidence_hash_ignores_known_order_insensitive_aspect_lists_only():
|
||||
left = {"aspects": {"gives": ["Mars", "Saturn"], "receives": ["Moon", "Sun"]}, "value": [2, 1]}
|
||||
right = {"value": [2, 1], "aspects": {"receives": ["Sun", "Moon"], "gives": ["Saturn", "Mars"]}}
|
||||
changed = {"value": [1, 2], "aspects": {"receives": ["Sun", "Moon"], "gives": ["Saturn", "Mars"]}}
|
||||
|
||||
assert semantic_evidence_hash(left) == semantic_evidence_hash(right)
|
||||
assert semantic_evidence_hash(left) != semantic_evidence_hash(changed)
|
||||
@@ -8,6 +8,22 @@ SCRIPTS = Path(__file__).resolve().parents[1] / "scripts"
|
||||
if str(SCRIPTS) not in sys.path:
|
||||
sys.path.insert(0, str(SCRIPTS))
|
||||
|
||||
from rectification_three_engine_packet import build_packet, case_hash
|
||||
|
||||
CASE = {"year": 1990, "month": 1, "day": 1, "hour": 12, "minute": 0, "lat": 0.0, "lon": 0.0, "tz": 0.0}
|
||||
|
||||
def test_packet_is_private_and_never_confirms(monkeypatch) -> None:
|
||||
import rectification_three_engine_packet as module
|
||||
monkeypatch.setattr(module, "_local_d1", lambda _: {"Sun": "Aries"})
|
||||
monkeypatch.setattr(module, "_pyjhora_d1", lambda _: {"Sun": "Aries"})
|
||||
monkeypatch.setattr(module, "_jyotishganit_d1", lambda _: {"Sun": "Aries"})
|
||||
packet = build_packet(CASE)
|
||||
assert packet["case_hash"] == case_hash(CASE)
|
||||
assert packet["input_contract_hash"]
|
||||
assert packet["stability_contract"]["minute_confirmation_allowed"] is False
|
||||
assert "year" not in str(packet)
|
||||
assert packet["can_confirm"] is False
|
||||
assert packet["vedastro"]["status"] == "requires_gateway_raw_archive"
|
||||
|
||||
def test_packet_queues_a_privacy_safe_vedastro_receipt(monkeypatch) -> None:
|
||||
import rectification_three_engine_packet as packet
|
||||
|
||||
Reference in New Issue
Block a user