fix(report): bind professional report hash to the delivered packet (BUG-693, BUG-694)
Independent Staging Quality Gate / validate (push) Successful in 9m45s
Independent Staging Quality Gate / publish (push) Successful in 2m14s

Rebind after sanitize, write an explicit binding_scope, and make the quality gate recompute coverage. Wall-clock fields stay in the packet but out of the hash.
This commit is contained in:
jesse-ux
2026-09-15 10:09:26 +08:00
parent 039b0a2608
commit 1d2aeffaef
8 changed files with 403 additions and 32 deletions
+52
View File
@@ -9,6 +9,19 @@ import re
from pathlib import Path
from typing import Any
try:
from scripts.calculation_profile_contract import (
ALLOWED_BINDING_EXCLUDED_PATHS,
ALLOWED_BINDING_EXCLUDED_TOP_KEYS,
hash_bound_result,
)
except ImportError: # pragma: no cover - direct-script execution
from calculation_profile_contract import (
ALLOWED_BINDING_EXCLUDED_PATHS,
ALLOWED_BINDING_EXCLUDED_TOP_KEYS,
hash_bound_result,
)
SCHEMA_VERSION = "jyotish.full_report_quality_gate.v1"
REPORT_SCHEMA = "pl9_style_professional_export_v1"
@@ -170,6 +183,45 @@ def evaluate_full_report(packet: dict[str, Any], rendered_markdown: str | None =
else:
checks.append(_check("provenance:result_binding", "passed", "input/result hashes match"))
scope = _as_dict(binding.get("binding_scope"))
if "excluded_top_keys" not in scope or "excluded_paths" not in scope:
blocking_reasons.append("provenance_binding_scope_absent")
checks.append(_check(
"provenance:result_binding_scope",
"blocked",
"provenance_binding_scope_absent",
))
else:
extra_top = {
str(item) for item in (scope.get("excluded_top_keys") or ())
} - set(ALLOWED_BINDING_EXCLUDED_TOP_KEYS)
extra_paths = {
str(item) for item in (scope.get("excluded_paths") or ())
} - set(ALLOWED_BINDING_EXCLUDED_PATHS)
if extra_top or extra_paths:
blocking_reasons.append("provenance_binding_scope_unexpected_exclusion")
checks.append(_check(
"provenance:result_binding_scope",
"blocked",
"provenance_binding_scope_unexpected_exclusion",
))
elif (
isinstance(expected_input_hash, str)
and hash_bound_result(packet, expected_input_hash, scope) != expected_result_hash
):
blocking_reasons.append("provenance_binding_scope_mismatch")
checks.append(_check(
"provenance:result_binding_scope",
"blocked",
"provenance_binding_scope_mismatch",
))
else:
checks.append(_check(
"provenance:result_binding_scope",
"passed",
"clipped payload matches result_hash",
))
chart_identity = _as_dict(packet.get("chart_identity"))
for key in (
"chart_profile_id",