From 62b699c1bdc4b85e22a1983f50364dd0cb19c855 Mon Sep 17 00:00:00 2001 From: 732642856 <732642856@qq.com> Date: Mon, 13 Jul 2026 23:00:56 +0800 Subject: [PATCH] require auditable external parity artifacts --- docs/research/pre_work_error_ledger.md | 1 + .../three_engine_parity_replay_validator.py | 30 +++++++++++++++++++ ...t_three_engine_parity_artifact_contract.py | 21 +++++++++++++ ...st_three_engine_parity_replay_validator.py | 10 ++++++- 4 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 tests/test_three_engine_parity_artifact_contract.py diff --git a/docs/research/pre_work_error_ledger.md b/docs/research/pre_work_error_ledger.md index 114a38b2..8ce219be 100644 --- a/docs/research/pre_work_error_ledger.md +++ b/docs/research/pre_work_error_ledger.md @@ -83,6 +83,7 @@ For large architecture or release work, also read: | ERR-050 | Prashna CLI/API/UI could synthesize or accept a non-question chart; legacy Tajika/Saham/Sphuta/Kunda paths also exposed approximate values as usable evidence. | mitigated 2026-07-12 | Require backend Swiss `PrashnaContext` with question text/time/location/timezone; reject client planets/ascendant. Block legacy Sphuta/Kunda/Gulika/Panchavargiya and no-location Saham paths; keep seven-planet Tajika interactions partial until named-yoga golden cases and formula parity exist. | | ERR-051 | Privacy redaction can replace executable numeric test fixtures with bare placeholder identifiers such as `REDACTED_YEAR`, causing `NameError` before a regression reaches its target. | observed 2026-07-12 | Public tests must use generic fixtures (for example 1990) or quoted placeholders only; run `rg -n "REDACTED_YEAR" tests` before release and repair executable occurrences. | | ERR-052 | Text-only privacy scanning cannot distinguish a harmless quoted placeholder from a bare Python identifier that will fail at runtime. | mitigated 2026-07-13 | `public_release_privacy_scan.py` parses shipped Python files and rejects executable `REDACTED_*` names; keep the AST regression test. | +| ERR-053 | A parity manifest could label an external engine `official_verified` without a raw artifact, hash, or calculation settings, making claimed oracle closure unverifiable. | mitigated 2026-07-13 | `three_engine_parity_replay_validator.py` requires raw artifact existence, SHA-256 and settings for verified/imported external engines; otherwise parity is `invalid`. | ## Fragment Sweep Command Set diff --git a/scripts/three_engine_parity_replay_validator.py b/scripts/three_engine_parity_replay_validator.py index 5d3a9ba5..74b18b42 100644 --- a/scripts/three_engine_parity_replay_validator.py +++ b/scripts/three_engine_parity_replay_validator.py @@ -5,6 +5,7 @@ from __future__ import annotations import argparse +import hashlib import json from pathlib import Path from typing import Any @@ -12,6 +13,33 @@ from typing import Any REQUIRED_ENGINES = {"VedAstro", "PyJHora_JHora", "jyotishganit"} REQUIRED_ROW_FIELDS = {"section", "field", "local_value", "oracle_values", "status"} VALID_ROW_STATUSES = {"match", "mismatch", "blocked", "not_comparable"} +RAW_VERIFIED_STATUSES = {"verified", "official_verified", "imported"} + + +def _artifact_errors(engine: str, payload: Any, manifest_dir: Path) -> list[dict[str, Any]]: + if not isinstance(payload, dict): + return [{"field": f"engines.{engine}", "error": "not_object"}] + if payload.get("status") not in RAW_VERIFIED_STATUSES: + return [] + raw_path = payload.get("official_raw_response_path") or payload.get("raw_output_path") + artifact_hash = payload.get("artifact_hash") + errors: list[dict[str, Any]] = [] + if not isinstance(raw_path, str) or not raw_path: + errors.append({"field": f"engines.{engine}.raw_output_path", "error": "required_for_verified_status"}) + return errors + if not isinstance(artifact_hash, str) or len(artifact_hash) != 64: + errors.append({"field": f"engines.{engine}.artifact_hash", "error": "sha256_required_for_verified_status"}) + return errors + artifact_path = (manifest_dir / raw_path).resolve() + if not artifact_path.is_file(): + errors.append({"field": f"engines.{engine}.raw_output_path", "error": "missing_artifact"}) + return errors + actual_hash = hashlib.sha256(artifact_path.read_bytes()).hexdigest() + if actual_hash != artifact_hash: + errors.append({"field": f"engines.{engine}.artifact_hash", "error": "hash_mismatch"}) + if not isinstance(payload.get("settings"), dict): + errors.append({"field": f"engines.{engine}.settings", "error": "required_for_verified_status"}) + return errors def _row_errors(row: Any, index: int) -> list[dict[str, Any]]: @@ -37,6 +65,8 @@ def validate_manifest(path: str | Path) -> dict[str, Any]: missing_engines = sorted(REQUIRED_ENGINES - set(engines)) for engine in missing_engines: errors.append({"field": f"engines.{engine}", "error": "missing"}) + for engine, payload in engines.items(): + errors.extend(_artifact_errors(engine, payload, manifest_path.parent)) if not isinstance(rows, list): rows = [] diff --git a/tests/test_three_engine_parity_artifact_contract.py b/tests/test_three_engine_parity_artifact_contract.py new file mode 100644 index 00000000..c55e5013 --- /dev/null +++ b/tests/test_three_engine_parity_artifact_contract.py @@ -0,0 +1,21 @@ +import json + +from scripts.three_engine_parity_replay_validator import validate_manifest + + +def test_verified_oracle_requires_raw_artifact_hash_and_settings(tmp_path): + manifest = { + "engines": { + "VedAstro": {"status": "official_verified"}, + "PyJHora_JHora": {"status": "blocked"}, + "jyotishganit": {"status": "blocked"}, + }, + "comparison_rows": [], + } + path = tmp_path / "manifest.json" + path.write_text(json.dumps(manifest), encoding="utf-8") + + result = validate_manifest(path) + + assert result["status"] == "invalid" + assert any(error["error"] == "required_for_verified_status" for error in result["errors"]) diff --git a/tests/test_three_engine_parity_replay_validator.py b/tests/test_three_engine_parity_replay_validator.py index 9fed6c0c..a42c65d0 100644 --- a/tests/test_three_engine_parity_replay_validator.py +++ b/tests/test_three_engine_parity_replay_validator.py @@ -1,5 +1,6 @@ from __future__ import annotations +import hashlib import json from pathlib import Path @@ -18,12 +19,19 @@ def test_three_engine_parity_manifest_blocks_without_oracle_rows() -> None: def test_three_engine_parity_validator_accepts_one_same_chart_row(tmp_path: Path) -> None: + raw = tmp_path / "vedastro.json" + raw.write_text('{"source":"official"}', encoding="utf-8") manifest = { "case_id": "public_same_chart_001", "birth_data_policy": "public_case_only", "status": "tested", "engines": { - "VedAstro": {"status": "official_verified", "official_raw_response_path": "references/oracle/artifacts/vedastro.json"}, + "VedAstro": { + "status": "official_verified", + "official_raw_response_path": "vedastro.json", + "artifact_hash": hashlib.sha256(raw.read_bytes()).hexdigest(), + "settings": {"ayanamsa": "lahiri"}, + }, "PyJHora_JHora": {"status": "tested", "raw_output_path": "references/oracle/artifacts/pyjhora.txt"}, "jyotishganit": {"status": "tested", "raw_output_path": "references/oracle/artifacts/jyotishganit.json"}, },