feat: integrate minute rectification safeguards
This commit is contained in:
@@ -217,3 +217,19 @@
|
||||
- 相关记录:BUG-009
|
||||
- 复发自:无
|
||||
- 修复版本:待提交(本地可测)
|
||||
|
||||
## BUG-012 | 分钟校正 safeguards PR 与主线证据契约分叉
|
||||
|
||||
- 状态:resolved
|
||||
- 首次发现:2026-07-22
|
||||
- 最近更新:2026-07-22
|
||||
- 影响面:分钟候选输入身份、公共 holdout 校验、三引擎证据包、PR quality gate
|
||||
- 用户现象:PR 与 `main` 在五个文件发生冲突,聚焦测试通过但完整 quick quality gate 在 API 安全测试收集阶段失败。
|
||||
- 触发条件:基于旧基线开发 v1 holdout safeguards,同时主线独立升级到 v2/v3 证据契约并移除旧 API store 导入。
|
||||
- 根因:PR 复用了旧 holdout 字段和 `true node` 默认,未执行与 CI 相同的 quick quality gate;主线已经采用 `mean node` 和更严格的 sealed holdout schema。
|
||||
- 修复:以主线为准新增兼容的输入指纹、邻近分钟探针和语义哈希;新增 v4 独立审核、日级事件、假分钟承诺门禁及非生产 intake;不恢复旧自动评估循环。
|
||||
- 验证:分钟校正聚焦回归、脚本直接执行检查、Ruff、Python compilation 和 quick quality gate。
|
||||
- 防复发:新 safeguards 必须以当前 schema 向前升级;候选身份必须继承产品计算默认;PR 验收必须包含 workflow 实际执行的 quick quality gate。
|
||||
- 相关记录:BUG-009、ERR-045、ERR-053、ERR-086
|
||||
- 复发自:无
|
||||
- 修复版本:待提交(本地可测)
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"schema_version": "minute-rectification-holdout-v4-intake",
|
||||
"status": "collecting_independently_reviewed_cases",
|
||||
"minimum_gate": {
|
||||
"public_aa_cases": 20,
|
||||
"events_per_case": 3,
|
||||
"domains_per_case": 2,
|
||||
"independent_event_sources_per_case": 2,
|
||||
"negative_minutes_per_case": 4,
|
||||
"day_precision_events_per_case": 3
|
||||
},
|
||||
"cases": [],
|
||||
"production_tuning_allowed": false,
|
||||
"verified_minute_claim_allowed": false,
|
||||
"boundary": "This intake queue is not a frozen holdout. Promotion requires a new version, a passed source audit and a scoring identity frozen before blind replay."
|
||||
}
|
||||
@@ -11,6 +11,14 @@ from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
try:
|
||||
from scripts.rectification_input_contract import (
|
||||
candidate_input_fingerprint,
|
||||
stability_probe_contract,
|
||||
)
|
||||
except ModuleNotFoundError: # pragma: no cover - direct script execution
|
||||
from rectification_input_contract import candidate_input_fingerprint, stability_probe_contract
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
ENGINE = ROOT / "scripts" / "jyotish_engine.py"
|
||||
@@ -60,6 +68,7 @@ def scan_candidate_times(payload: dict[str, Any], *, uncertainty_minutes: int =
|
||||
rows.append({
|
||||
"time": moment.strftime("%Y-%m-%d %H:%M"),
|
||||
"offset_minutes": offset,
|
||||
"input_fingerprint": candidate_input_fingerprint(point),
|
||||
"d1_ascendant": asc.get("sign"),
|
||||
"d1_degree_in_sign": asc.get("degree_in_sign"),
|
||||
"divisional_ascendants": divisional,
|
||||
@@ -68,14 +77,22 @@ def scan_candidate_times(payload: dict[str, Any], *, uncertainty_minutes: int =
|
||||
unavailable_vargas = [varga.upper() for varga in _VARGAS if all(row["divisional_ascendants"][varga.upper()] is None for row in rows)]
|
||||
supported_vargas = [varga.lower() for varga in _VARGAS if varga.upper() not in unavailable_vargas]
|
||||
modal = Counter(signatures).most_common(1)[0][0]
|
||||
for row, signature in zip(rows, signatures):
|
||||
row["sensitivity_count"] = sum(left != right for left, right in zip(signature, modal))
|
||||
for row, signature in zip(rows, signatures, strict=True):
|
||||
row["sensitivity_count"] = sum(
|
||||
left != right for left, right in zip(signature, modal, strict=True)
|
||||
)
|
||||
row["sensitive_layers"] = [
|
||||
name for name, current, typical in zip(("D1", "D4", "D9", "D10", "D24", "D30"), signature, modal)
|
||||
name
|
||||
for name, current, typical in zip(
|
||||
("D1", "D4", "D9", "D10", "D24", "D30"),
|
||||
signature,
|
||||
modal,
|
||||
strict=True,
|
||||
)
|
||||
if current != typical
|
||||
]
|
||||
transitions = []
|
||||
for previous, current in zip(rows, rows[1:]):
|
||||
for previous, current in zip(rows, rows[1:], strict=False):
|
||||
changed = [name for name in ("d1_ascendant", "divisional_ascendants") if previous[name] != current[name]]
|
||||
if changed:
|
||||
transitions.append({"between": [previous["time"], current["time"]], "changed": changed})
|
||||
@@ -88,6 +105,17 @@ 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").strip().lower(),
|
||||
"node_mode": str(
|
||||
payload.get("node_mode", payload.get("nodeMode", "mean"))
|
||||
).strip().lower(),
|
||||
},
|
||||
},
|
||||
"stability_contract": stability_probe_contract(payload),
|
||||
"transitions": transitions,
|
||||
"supported_vargas": [varga.upper() for varga in supported_vargas],
|
||||
"unavailable_vargas": unavailable_vargas,
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Append one independently reviewed case to a non-production v4 intake queue."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
try:
|
||||
from scripts.minute_rectification_holdout_validator import case_errors
|
||||
except ModuleNotFoundError: # pragma: no cover - direct script execution
|
||||
from minute_rectification_holdout_validator import case_errors
|
||||
|
||||
INTAKE_SCHEMA_VERSION = "minute-rectification-holdout-v4-intake"
|
||||
DEFAULT_INTAKE = (
|
||||
Path(__file__).resolve().parents[1]
|
||||
/ "references"
|
||||
/ "real_case_calibration"
|
||||
/ "minute_rectification_holdout_v4_intake.json"
|
||||
)
|
||||
|
||||
|
||||
def append_case(path: Path, case: dict[str, Any]) -> dict[str, Any]:
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
if data.get("schema_version") != INTAKE_SCHEMA_VERSION:
|
||||
return {"appended": False, "errors": ["intake_schema_required"]}
|
||||
gate = data.get("minimum_gate") if isinstance(data.get("minimum_gate"), dict) else {}
|
||||
cases = data.get("cases") if isinstance(data.get("cases"), list) else []
|
||||
case_id = str(case.get("case_id") or "").strip()
|
||||
if not case_id:
|
||||
return {"appended": False, "errors": ["missing_case_id"]}
|
||||
if any(isinstance(existing, dict) and existing.get("case_id") == case_id for existing in cases):
|
||||
return {"appended": False, "errors": ["duplicate_case_id"]}
|
||||
errors = case_errors(case, gate, require_review_safeguards=True)
|
||||
if errors:
|
||||
return {"appended": False, "errors": errors}
|
||||
|
||||
cases.append({**case, "ingested_at": datetime.now(UTC).isoformat().replace("+00:00", "Z")})
|
||||
data["cases"] = cases
|
||||
data["status"] = "collecting_independently_reviewed_cases"
|
||||
data["production_tuning_allowed"] = False
|
||||
data["verified_minute_claim_allowed"] = False
|
||||
path.write_text(json.dumps(data, ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||
return {
|
||||
"appended": True,
|
||||
"errors": [],
|
||||
"case_count": len(cases),
|
||||
"verified_minute_claim_allowed": False,
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("manifest", nargs="?", type=Path, default=DEFAULT_INTAKE)
|
||||
parser.add_argument("--case-json", required=True)
|
||||
args = parser.parse_args()
|
||||
result = append_case(args.manifest, json.loads(args.case_json))
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
|
||||
return 0 if result["appended"] else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -13,6 +13,7 @@ DEFAULT_MANIFEST = ROOT / "references" / "real_case_calibration" / "minute_recti
|
||||
SUPPORTED_SCHEMA_VERSIONS = {
|
||||
"minute-rectification-holdout-v2",
|
||||
"minute-rectification-holdout-v3",
|
||||
"minute-rectification-holdout-v4",
|
||||
}
|
||||
ALLOWED_DOMAINS = {
|
||||
"education", "relocation", "relationship", "career", "finance", "health_pressure",
|
||||
@@ -142,6 +143,71 @@ def _case_errors(case: Any, gate: dict[str, Any]) -> list[str]:
|
||||
return errors
|
||||
|
||||
|
||||
def _review_safeguard_errors(case: Any, gate: dict[str, Any]) -> list[str]:
|
||||
"""Validate safeguards required for newly admitted v4 holdout cases."""
|
||||
if not isinstance(case, dict):
|
||||
return ["case_must_be_object"]
|
||||
errors: list[str] = []
|
||||
if not isinstance(case.get("adjudicator"), str) or not case.get("adjudicator", "").strip():
|
||||
errors.append("missing_independent_adjudicator")
|
||||
if case.get("independent_human_reviewed") is not True:
|
||||
errors.append("independent_review_not_attested")
|
||||
if case.get("frozen_before_scoring") is not True:
|
||||
errors.append("case_not_frozen_before_scoring")
|
||||
|
||||
events = case.get("events") if isinstance(case.get("events"), list) else []
|
||||
day_precision_count = sum(
|
||||
isinstance(event, dict)
|
||||
and event.get("precision") == "day"
|
||||
and _parse_event_date(event.get("date"), event.get("precision")) is not None
|
||||
for event in events
|
||||
)
|
||||
if day_precision_count < int(gate.get("day_precision_events_per_case", 3)):
|
||||
errors.append("insufficient_day_precision_events")
|
||||
|
||||
offsets = case.get("false_minute_offsets") if isinstance(case.get("false_minute_offsets"), list) else []
|
||||
commitments = (
|
||||
case.get("false_minute_commitments")
|
||||
if isinstance(case.get("false_minute_commitments"), list)
|
||||
else []
|
||||
)
|
||||
committed_offsets: list[int] = []
|
||||
hashes: list[str] = []
|
||||
for item in commitments:
|
||||
if not isinstance(item, dict):
|
||||
errors.append("invalid_false_minute_commitment")
|
||||
continue
|
||||
offset = item.get("offset_minutes")
|
||||
commitment_hash = item.get("commitment_hash")
|
||||
if not isinstance(offset, int) or offset == 0:
|
||||
errors.append("invalid_false_minute_commitment_offset")
|
||||
else:
|
||||
committed_offsets.append(offset)
|
||||
if (
|
||||
not isinstance(commitment_hash, str)
|
||||
or len(commitment_hash) != 64
|
||||
or any(character not in "0123456789abcdef" for character in commitment_hash.lower())
|
||||
):
|
||||
errors.append("invalid_false_minute_commitment_hash")
|
||||
else:
|
||||
hashes.append(commitment_hash.lower())
|
||||
if any(key in item for key in ("candidate_minute", "published_minute", "birth_time")):
|
||||
errors.append("false_minute_commitment_leaks_time")
|
||||
if sorted(committed_offsets) != sorted(offsets):
|
||||
errors.append("false_minute_commitments_do_not_match_offsets")
|
||||
if len(hashes) != len(set(hashes)):
|
||||
errors.append("duplicate_false_minute_commitment_hash")
|
||||
return errors
|
||||
|
||||
|
||||
def case_errors(case: Any, gate: dict[str, Any], *, require_review_safeguards: bool = False) -> list[str]:
|
||||
"""Public case-level validator shared by frozen manifests and intake tooling."""
|
||||
errors = _case_errors(case, gate)
|
||||
if require_review_safeguards:
|
||||
errors.extend(_review_safeguard_errors(case, gate))
|
||||
return sorted(set(errors))
|
||||
|
||||
|
||||
def validate(manifest_path: Path = DEFAULT_MANIFEST) -> dict[str, Any]:
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
gate = manifest.get("minimum_gate") if isinstance(manifest.get("minimum_gate"), dict) else {}
|
||||
@@ -154,7 +220,10 @@ def validate(manifest_path: Path = DEFAULT_MANIFEST) -> dict[str, Any]:
|
||||
if manifest.get("frozen_before_replay") is not True:
|
||||
manifest_errors.append("benchmark_not_frozen_before_replay")
|
||||
if (
|
||||
manifest.get("schema_version") == "minute-rectification-holdout-v3"
|
||||
manifest.get("schema_version") in {
|
||||
"minute-rectification-holdout-v3",
|
||||
"minute-rectification-holdout-v4",
|
||||
}
|
||||
and manifest.get("source_audit_status") != "passed_before_freeze"
|
||||
):
|
||||
manifest_errors.append("source_content_audit_not_passed_before_freeze")
|
||||
@@ -165,8 +234,9 @@ def validate(manifest_path: Path = DEFAULT_MANIFEST) -> dict[str, Any]:
|
||||
seen_ids: set[str] = set()
|
||||
invalid_details: list[dict[str, Any]] = []
|
||||
valid_cases = 0
|
||||
require_review_safeguards = manifest.get("schema_version") == "minute-rectification-holdout-v4"
|
||||
for case in cases:
|
||||
errors = _case_errors(case, gate)
|
||||
errors = case_errors(case, gate, require_review_safeguards=require_review_safeguards)
|
||||
case_id = case.get("case_id") if isinstance(case, dict) else "non_object_case"
|
||||
if isinstance(case_id, str) and case_id in seen_ids:
|
||||
errors.append("duplicate_case_id")
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Audit reusable public AA cases before admitting them to a minute holdout."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
DEFAULT_SOURCES = (
|
||||
ROOT / "references" / "real_case_calibration" / "replay_manifest.json",
|
||||
ROOT / "references" / "real_case_calibration" / "replay_manifest_holdout_v2.json",
|
||||
ROOT / "references" / "real_case_calibration" / "replay_manifest_probe3_v2.json",
|
||||
ROOT / "references" / "real_case_calibration" / "public_context_manifest.json",
|
||||
)
|
||||
|
||||
|
||||
def _events(case: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
raw = case.get("events") if isinstance(case.get("events"), list) else case.get("event_outcomes")
|
||||
return [event for event in raw or [] if isinstance(event, dict)]
|
||||
|
||||
|
||||
def _birth_source(subject: dict[str, Any]) -> dict[str, Any]:
|
||||
if isinstance(subject.get("birth_source"), dict):
|
||||
return subject["birth_source"]
|
||||
birth = subject.get("birth") if isinstance(subject.get("birth"), dict) else {}
|
||||
return birth.get("source") if isinstance(birth.get("source"), dict) else {}
|
||||
|
||||
|
||||
def _is_aa(source: dict[str, Any]) -> bool:
|
||||
return source.get("time_accuracy_rating") == "AA" or source.get("rodden_rating") == "AA"
|
||||
|
||||
|
||||
def build_source_audit(paths: list[Path] | tuple[Path, ...] = DEFAULT_SOURCES) -> dict[str, Any]:
|
||||
entries: dict[str, dict[str, Any]] = {}
|
||||
for path in paths:
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
for case in data.get("cases", []):
|
||||
if not isinstance(case, dict):
|
||||
continue
|
||||
subject = case.get("subject") if isinstance(case.get("subject"), dict) else case
|
||||
source = _birth_source(subject)
|
||||
source_url = str(source.get("url") or "")
|
||||
if not _is_aa(source) or not source_url:
|
||||
continue
|
||||
label = str(subject.get("name") or subject.get("subject_label") or case.get("case_id") or "unnamed")
|
||||
entry = entries.setdefault(source_url, {
|
||||
"subject": label,
|
||||
"birth_source_url": source_url,
|
||||
"case_ids": [],
|
||||
"day_precision_event_dates": set(),
|
||||
})
|
||||
entry["case_ids"].append(str(case.get("case_id") or label))
|
||||
for event in _events(case):
|
||||
event_date = str(event.get("event_date") or event.get("date") or "")
|
||||
if len(event_date) == 10:
|
||||
entry["day_precision_event_dates"].add(event_date)
|
||||
|
||||
cases = []
|
||||
for entry in entries.values():
|
||||
event_count = len(entry["day_precision_event_dates"])
|
||||
cases.append({
|
||||
"subject": entry["subject"],
|
||||
"birth_source_url": entry["birth_source_url"],
|
||||
"case_ids": sorted(set(entry["case_ids"])),
|
||||
"existing_day_precision_event_count": event_count,
|
||||
"additional_day_precision_events_required": max(0, 3 - event_count),
|
||||
"review_and_commitment_controls_required": True,
|
||||
})
|
||||
cases.sort(key=lambda case: (case["additional_day_precision_events_required"], case["subject"]))
|
||||
return {
|
||||
"scope": "minute_rectification_public_aa_source_audit",
|
||||
"public_aa_case_count": len(cases),
|
||||
"minimum_public_aa_cases": 20,
|
||||
"additional_public_aa_cases_required": max(0, 20 - len(cases)),
|
||||
"cases": cases,
|
||||
"production_tuning_allowed": False,
|
||||
"boundary": (
|
||||
"Source discovery is not holdout validation. Every promoted case still requires "
|
||||
"independent review, day-precision events and committed false-minute controls."
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("sources", nargs="*", type=Path, default=list(DEFAULT_SOURCES))
|
||||
parser.add_argument("--output", type=Path)
|
||||
args = parser.parse_args()
|
||||
result = build_source_audit(args.sources)
|
||||
text = json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True)
|
||||
if args.output:
|
||||
args.output.write_text(text + "\n", encoding="utf-8")
|
||||
print(text)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,113 @@
|
||||
"""Stable, privacy-safe identities for birth-time rectification evidence."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any
|
||||
|
||||
INPUT_CONTRACT_VERSION = "rectification-input-v1"
|
||||
REQUIRED_FIELDS = ("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]:
|
||||
"""Normalize only calculation-bearing fields using deployed defaults."""
|
||||
missing = [field for field in REQUIRED_FIELDS 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)
|
||||
node_mode = str(case.get("node_mode", case.get("nodeMode", "mean"))).strip().lower()
|
||||
if node_mode not in {"mean", "true"}:
|
||||
raise ValueError("node_mode must be mean or true")
|
||||
|
||||
return {
|
||||
"year": year,
|
||||
"month": month,
|
||||
"day": day,
|
||||
"hour": hour,
|
||||
"minute": minute,
|
||||
"second": second,
|
||||
"lat": float(case["lat"]),
|
||||
"lon": float(case["lon"]),
|
||||
"tz": float(case["tz"]),
|
||||
"ayanamsa": str(case.get("ayanamsa", "lahiri")).strip().lower(),
|
||||
"node_mode": node_mode,
|
||||
}
|
||||
|
||||
|
||||
def candidate_input_fingerprint(case: dict[str, Any]) -> str:
|
||||
payload = {
|
||||
"schema_version": INPUT_CONTRACT_VERSION,
|
||||
"calculation_input": canonical_birth_input(case),
|
||||
}
|
||||
return hashlib.sha256(_canonical_json(payload).encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def stability_probe_contract(case: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Materialize adjacent-minute identities without claiming that they passed."""
|
||||
baseline = canonical_birth_input(case)
|
||||
center = datetime(
|
||||
baseline["year"],
|
||||
baseline["month"],
|
||||
baseline["day"],
|
||||
baseline["hour"],
|
||||
baseline["minute"],
|
||||
baseline["second"],
|
||||
)
|
||||
probes = []
|
||||
for offset in STABILITY_OFFSETS:
|
||||
moment = center + timedelta(minutes=offset)
|
||||
probe = {
|
||||
**baseline,
|
||||
"year": moment.year,
|
||||
"month": moment.month,
|
||||
"day": moment.day,
|
||||
"hour": moment.hour,
|
||||
"minute": moment.minute,
|
||||
"second": moment.second,
|
||||
}
|
||||
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(baseline),
|
||||
"probes": probes,
|
||||
"minute_confirmation_allowed": False,
|
||||
"blocker": "public_blind_minute_holdout_not_closed",
|
||||
"boundary": (
|
||||
"Probe identities are reproducible inputs, not evidence that a minute 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 while raw artifact hashes remain intact."""
|
||||
normalized = _semantic_normalize(value)
|
||||
return hashlib.sha256(_canonical_json(normalized).encode("utf-8")).hexdigest()
|
||||
@@ -1,9 +1,7 @@
|
||||
"""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
|
||||
@@ -11,6 +9,19 @@ from typing import Any
|
||||
|
||||
from domain_calculation_service import compute_chart
|
||||
|
||||
try:
|
||||
from scripts.rectification_input_contract import (
|
||||
candidate_input_fingerprint,
|
||||
canonical_birth_input,
|
||||
stability_probe_contract,
|
||||
)
|
||||
except ModuleNotFoundError: # pragma: no cover - direct script execution
|
||||
from rectification_input_contract import (
|
||||
candidate_input_fingerprint,
|
||||
canonical_birth_input,
|
||||
stability_probe_contract,
|
||||
)
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
JYOTISHGANIT_ROOT = ROOT / "references" / "open_source_sources" / "jyotishganit"
|
||||
PLANETS = ("Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn")
|
||||
@@ -19,29 +30,12 @@ SIGNS = ("Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo", "Libra", "Scorpi
|
||||
|
||||
def canonical_case_input(case: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Normalize only calculation-bearing fields before hashing or engine dispatch."""
|
||||
required = ("year", "month", "day", "hour", "minute", "lat", "lon", "tz")
|
||||
missing = [key for key in required if key not in case]
|
||||
if missing:
|
||||
raise ValueError("case is missing required birth fields")
|
||||
return {
|
||||
"year": int(case["year"]),
|
||||
"month": int(case["month"]),
|
||||
"day": int(case["day"]),
|
||||
"hour": int(case["hour"]),
|
||||
"minute": int(case["minute"]),
|
||||
"second": int(case.get("second", 0)),
|
||||
"lat": float(case["lat"]),
|
||||
"lon": float(case["lon"]),
|
||||
"tz": float(case["tz"]),
|
||||
"ayanamsa": str(case.get("ayanamsa", "lahiri")).strip().lower(),
|
||||
"node_mode": str(case.get("node_mode", case.get("nodeMode", "mean"))).strip().lower(),
|
||||
}
|
||||
return canonical_birth_input(case)
|
||||
|
||||
|
||||
def case_hash(case: dict[str, Any]) -> str:
|
||||
"""Stable identity for evidence correlation; never exposes birth data."""
|
||||
payload = json.dumps(canonical_case_input(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]:
|
||||
@@ -140,6 +134,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),
|
||||
|
||||
@@ -14,9 +14,10 @@ ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
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 benchmarks.jyotish.scripts.run_pyjhora_compare import build_pyjhora_sample # noqa: E402
|
||||
from benchmarks.jyotish.scripts.run_skill_baseline import run_sample # noqa: E402
|
||||
from scripts.rectification_input_contract import semantic_evidence_hash # noqa: E402
|
||||
from scripts.three_engine_parity_runner import _capture_jyotishganit_raw # noqa: E402
|
||||
|
||||
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": {
|
||||
|
||||
@@ -1570,6 +1570,7 @@ def test_capability_audit_scans_registry_and_local_sources() -> None:
|
||||
assert audit['surfaces']['app_routes'] == ['admin/codes', 'home', 'login']
|
||||
assert set(audit['surfaces']['app_visible_topics']) == {
|
||||
'Birth Rectification',
|
||||
'Case Validation',
|
||||
'Synastry 16-factor',
|
||||
}
|
||||
assert '/api/deep_varga_avastha' in audit['surfaces']['api_endpoints']
|
||||
@@ -1599,14 +1600,14 @@ def test_capability_audit_scans_registry_and_local_sources() -> None:
|
||||
assert all('ux_next_action' in row for row in ux['next_queue'])
|
||||
ux_by_id = {row['id']: row for row in ux['rows']}
|
||||
assert ux_by_id['birth_time_rectifier']['ux_level'] in {'excellent', 'usable'}
|
||||
assert ux_by_id['case_validator']['ux_level'] == 'excellent'
|
||||
assert ux_by_id['synastry_16factor']['ux_level'] == 'excellent'
|
||||
for technique_id in [
|
||||
'ashtakavarga_pav',
|
||||
'ashtakavarga_sodhita',
|
||||
'bhava_bala',
|
||||
'career_engine',
|
||||
'case_validator',
|
||||
'deep_varga_avastha',
|
||||
'deep_varga_avastha',
|
||||
'divisional_yoga',
|
||||
'kakshya',
|
||||
'kp_system',
|
||||
|
||||
@@ -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"]["settings"]["node_mode"] == "mean"
|
||||
assert report["rows"][0]["input_fingerprint"] != report["rows"][1]["input_fingerprint"]
|
||||
assert report["stability_contract"]["minute_confirmation_allowed"] is False
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import hashlib
|
||||
import json
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
|
||||
from scripts.minute_rectification_holdout_intake import DEFAULT_INTAKE, append_case
|
||||
from scripts.minute_rectification_holdout_validator import DEFAULT_MANIFEST
|
||||
|
||||
|
||||
def _reviewed_case() -> dict:
|
||||
case = deepcopy(json.loads(DEFAULT_MANIFEST.read_text(encoding="utf-8"))["cases"][0])
|
||||
case["case_id"] = "new-reviewed-case"
|
||||
case["adjudicator"] = "independent-reviewer"
|
||||
case["independent_human_reviewed"] = True
|
||||
case["frozen_before_scoring"] = True
|
||||
case["false_minute_commitments"] = [
|
||||
{
|
||||
"offset_minutes": offset,
|
||||
"commitment_hash": hashlib.sha256(f"control:{offset}".encode()).hexdigest(),
|
||||
}
|
||||
for offset in case["false_minute_offsets"]
|
||||
]
|
||||
return case
|
||||
|
||||
|
||||
def _queue(path: Path) -> None:
|
||||
path.write_text(json.dumps({
|
||||
"schema_version": "minute-rectification-holdout-v4-intake",
|
||||
"minimum_gate": {
|
||||
"events_per_case": 3,
|
||||
"domains_per_case": 2,
|
||||
"independent_event_sources_per_case": 2,
|
||||
"negative_minutes_per_case": 4,
|
||||
"day_precision_events_per_case": 3,
|
||||
},
|
||||
"cases": [],
|
||||
}), encoding="utf-8")
|
||||
|
||||
|
||||
def test_intake_appends_reviewed_case_but_keeps_release_blocked(tmp_path: Path) -> None:
|
||||
path = tmp_path / "intake.json"
|
||||
_queue(path)
|
||||
|
||||
report = append_case(path, _reviewed_case())
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
assert report["appended"] is True
|
||||
assert report["verified_minute_claim_allowed"] is False
|
||||
assert data["production_tuning_allowed"] is False
|
||||
assert data["verified_minute_claim_allowed"] is False
|
||||
assert data["cases"][0]["ingested_at"].endswith("Z")
|
||||
|
||||
|
||||
def test_intake_rejects_missing_review_and_commitments(tmp_path: Path) -> None:
|
||||
path = tmp_path / "intake.json"
|
||||
_queue(path)
|
||||
case = _reviewed_case()
|
||||
case["independent_human_reviewed"] = False
|
||||
case["false_minute_commitments"] = []
|
||||
|
||||
report = append_case(path, case)
|
||||
|
||||
assert report["appended"] is False
|
||||
assert "independent_review_not_attested" in report["errors"]
|
||||
assert "false_minute_commitments_do_not_match_offsets" in report["errors"]
|
||||
|
||||
|
||||
def test_default_intake_is_non_production_and_empty() -> None:
|
||||
data = json.loads(DEFAULT_INTAKE.read_text(encoding="utf-8"))
|
||||
|
||||
assert data["cases"] == []
|
||||
assert data["production_tuning_allowed"] is False
|
||||
assert data["verified_minute_claim_allowed"] is False
|
||||
@@ -1,3 +1,4 @@
|
||||
import hashlib
|
||||
import json
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
@@ -49,6 +50,52 @@ def test_v3_validator_requires_content_source_audit_before_freeze(tmp_path: Path
|
||||
assert report["status"] == "blocked_awaiting_public_aa_cases"
|
||||
|
||||
|
||||
def _add_v4_review_safeguards(manifest: dict) -> None:
|
||||
manifest["schema_version"] = "minute-rectification-holdout-v4"
|
||||
manifest["source_audit_status"] = "passed_before_freeze"
|
||||
manifest["minimum_gate"]["day_precision_events_per_case"] = 3
|
||||
for case in manifest["cases"]:
|
||||
case["adjudicator"] = "independent-reviewer"
|
||||
case["independent_human_reviewed"] = True
|
||||
case["frozen_before_scoring"] = True
|
||||
case["false_minute_commitments"] = [
|
||||
{
|
||||
"offset_minutes": offset,
|
||||
"commitment_hash": hashlib.sha256(
|
||||
f"{case['case_id']}:{offset}:sealed".encode()
|
||||
).hexdigest(),
|
||||
}
|
||||
for offset in case["false_minute_offsets"]
|
||||
]
|
||||
|
||||
|
||||
def test_v4_validator_accepts_reviewed_cases_with_committed_false_minutes(tmp_path: Path) -> None:
|
||||
manifest = deepcopy(_manifest())
|
||||
_add_v4_review_safeguards(manifest)
|
||||
path = tmp_path / "v4.json"
|
||||
path.write_text(json.dumps(manifest), encoding="utf-8")
|
||||
|
||||
report = validate(path)
|
||||
|
||||
assert report["manifest_errors"] == []
|
||||
assert report["invalid_case_details"] == []
|
||||
|
||||
|
||||
def test_v4_validator_rejects_unreviewed_or_uncommitted_cases(tmp_path: Path) -> None:
|
||||
manifest = deepcopy(_manifest())
|
||||
_add_v4_review_safeguards(manifest)
|
||||
case = manifest["cases"][0]
|
||||
case["independent_human_reviewed"] = False
|
||||
case["false_minute_commitments"].pop()
|
||||
path = tmp_path / "invalid-v4.json"
|
||||
path.write_text(json.dumps(manifest), encoding="utf-8")
|
||||
|
||||
errors = validate(path)["invalid_case_details"][0]["errors"]
|
||||
|
||||
assert "independent_review_not_attested" in errors
|
||||
assert "false_minute_commitments_do_not_match_offsets" in errors
|
||||
|
||||
|
||||
def test_validator_rejects_tuning_case_and_non_independent_event_source(tmp_path: Path) -> None:
|
||||
manifest = deepcopy(_manifest())
|
||||
case = manifest["cases"][0]
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from scripts.minute_rectification_source_audit import build_source_audit
|
||||
|
||||
|
||||
def test_source_audit_supports_current_holdout_shape_and_counts_day_precision(tmp_path: Path) -> None:
|
||||
source = tmp_path / "cases.json"
|
||||
source.write_text(json.dumps({"cases": [{
|
||||
"case_id": "public-case",
|
||||
"subject_label": "Public case",
|
||||
"birth": {"source": {"rodden_rating": "AA", "url": "https://example.test/birth"}},
|
||||
"events": [
|
||||
{"date": "2001-01-01"},
|
||||
{"date": "2002"},
|
||||
],
|
||||
}]}), encoding="utf-8")
|
||||
|
||||
report = build_source_audit([source])
|
||||
|
||||
assert report["public_aa_case_count"] == 1
|
||||
assert report["cases"][0]["existing_day_precision_event_count"] == 1
|
||||
assert report["cases"][0]["additional_day_precision_events_required"] == 2
|
||||
assert report["production_tuning_allowed"] is False
|
||||
@@ -0,0 +1,47 @@
|
||||
from scripts.rectification_input_contract import (
|
||||
candidate_input_fingerprint,
|
||||
canonical_birth_input,
|
||||
semantic_evidence_hash,
|
||||
stability_probe_contract,
|
||||
)
|
||||
|
||||
CASE = {
|
||||
"year": 1990,
|
||||
"month": 1,
|
||||
"day": 1,
|
||||
"hour": 12,
|
||||
"minute": 0,
|
||||
"lat": 0.0,
|
||||
"lon": 0.0,
|
||||
"tz": 0.0,
|
||||
}
|
||||
|
||||
|
||||
def test_contract_uses_deployed_mean_node_default_and_stable_identity() -> None:
|
||||
reordered = {key: CASE[key] for key in reversed(CASE)}
|
||||
|
||||
assert canonical_birth_input(CASE)["node_mode"] == "mean"
|
||||
assert candidate_input_fingerprint(CASE) == candidate_input_fingerprint(reordered)
|
||||
assert candidate_input_fingerprint(CASE) == candidate_input_fingerprint({**CASE, "nodeMode": "MEAN"})
|
||||
|
||||
|
||||
def test_candidate_fingerprint_changes_with_calculation_input() -> None:
|
||||
assert candidate_input_fingerprint(CASE) != candidate_input_fingerprint({**CASE, "minute": 1})
|
||||
assert candidate_input_fingerprint(CASE) != candidate_input_fingerprint({**CASE, "node_mode": "true"})
|
||||
|
||||
|
||||
def test_stability_contract_records_adjacent_probes_without_confirming() -> None:
|
||||
contract = stability_probe_contract(CASE)
|
||||
|
||||
assert [probe["offset_minutes"] for probe in contract["probes"]] == [-5, -2, -1, 1, 2, 5]
|
||||
assert contract["minute_confirmation_allowed"] is False
|
||||
assert contract["status"] == "pending_score_comparison"
|
||||
|
||||
|
||||
def test_semantic_hash_normalizes_only_known_order_insensitive_lists() -> None:
|
||||
left = {"aspects": {"gives": ["Mars", "Saturn"]}, "ordered_scores": [2, 1]}
|
||||
reordered_aspects = {"ordered_scores": [2, 1], "aspects": {"gives": ["Saturn", "Mars"]}}
|
||||
reordered_scores = {"ordered_scores": [1, 2], "aspects": {"gives": ["Saturn", "Mars"]}}
|
||||
|
||||
assert semantic_evidence_hash(left) == semantic_evidence_hash(reordered_aspects)
|
||||
assert semantic_evidence_hash(left) != semantic_evidence_hash(reordered_scores)
|
||||
@@ -1,6 +1,5 @@
|
||||
from scripts.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}
|
||||
|
||||
|
||||
@@ -10,6 +9,8 @@ def test_packet_is_private_and_never_confirms(monkeypatch) -> None:
|
||||
monkeypatch.setattr("scripts.rectification_three_engine_packet._jyotishganit_d1", lambda _: {"Sun": "Aries"})
|
||||
packet = build_packet(CASE)
|
||||
assert packet["case_hash"] == case_hash(CASE)
|
||||
assert packet["input_contract_hash"] == packet["case_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"
|
||||
|
||||
Reference in New Issue
Block a user