fix: canonicalize provider review asset hashes
Build and Test Quality Gate / Contract Web typecheck and build (pull_request) Successful in 9m31s
Build and Test Quality Gate / Python tests and Contract API migration (pull_request) Failing after 19m12s
Security Quality Gate / npm audit, Bandit, pip-audit, detect-secrets (pull_request) Successful in 5m5s

This commit is contained in:
2026-08-11 21:12:59 +08:00
parent 600656cbf6
commit d6a6752258
6 changed files with 39 additions and 8 deletions
@@ -12,6 +12,7 @@ from urllib.parse import urlparse
from pydantic import BaseModel, Field
from yuqei_ai_platform_api.provider_legal_approval import SignedProviderLegalApproval
from yuqei_ai_platform_api.provider_review_package import canonical_review_asset_bytes
ACCEPTANCE_POLICY_VERSION = "phase13-provider-acceptance-v1"
@@ -385,7 +386,7 @@ def _manifest_hash(paths: list[Path]) -> str:
for path in paths:
digest.update(path.name.encode("utf-8"))
digest.update(b"\0")
digest.update(path.read_bytes())
digest.update(canonical_review_asset_bytes(path))
digest.update(b"\0")
return digest.hexdigest()
@@ -9,12 +9,14 @@ from __future__ import annotations
import hashlib
import json
from datetime import UTC, datetime
from pathlib import Path
from typing import Any, Literal
from pydantic import BaseModel, ConfigDict, Field
REVIEW_PACKAGE_VERSION = "provider-technical-review-package-v1"
CANONICAL_TEXT_ASSET_SUFFIXES = frozenset({".csv", ".json", ".md"})
class StrictModel(BaseModel):
@@ -82,6 +84,14 @@ def sha256_bytes(value: bytes) -> str:
return hashlib.sha256(value).hexdigest()
def canonical_review_asset_bytes(path: Path) -> bytes:
"""Return stable review bytes independent of checkout line endings."""
raw = path.read_bytes()
if path.suffix.lower() in CANONICAL_TEXT_ASSET_SUFFIXES:
return raw.replace(b"\r\n", b"\n").replace(b"\r", b"\n")
return raw
def _as_utc(value: datetime) -> datetime:
if value.tzinfo is None or value.utcoffset() is None:
raise ValueError("review package timestamps must be timezone-aware")
@@ -44,6 +44,7 @@ from yuqei_ai_platform_api.provider_acceptance import expected_acceptance_invent
from yuqei_ai_platform_api.provider_review_package import ( # noqa: E402
ProviderTechnicalReviewPackage,
canonical_json,
canonical_review_asset_bytes,
canonical_review_package,
review_package_sha256,
sha256_bytes,
@@ -170,7 +171,7 @@ def _validate_provider_package_inventory(package: ProviderTechnicalReviewPackage
expected_assets: dict[str, tuple[str, int]] = {}
for asset_path in sorted(item for item in PHASE13_ROOT.rglob("*") if item.is_file()):
raw = asset_path.read_bytes()
raw = canonical_review_asset_bytes(asset_path)
expected_assets[asset_path.relative_to(PHASE13_ROOT).as_posix()] = (
sha256_bytes(raw),
len(raw),
@@ -211,7 +212,7 @@ def _safe_asset(relative: str) -> tuple[str, str, int]:
raise ValueError("contract product golden asset escapes its root") from error
if not candidate.is_file():
raise ValueError("contract product golden asset is not a regular file")
raw = candidate.read_bytes()
raw = canonical_review_asset_bytes(candidate)
return relative.replace("\\", "/"), sha256_bytes(raw), len(raw)
@@ -276,7 +277,7 @@ def build_product_golden_evidence(summary_file: Path) -> dict[str, Any]:
}
)
cases.sort(key=lambda item: (item["suite"], item["sample_id"]))
inventory_raw = GENERATION_INVENTORY.read_bytes()
inventory_raw = canonical_review_asset_bytes(GENERATION_INVENTORY)
return {
"summary_file_sha256": sha256_bytes(summary_raw),
"inventory_file_sha256": sha256_bytes(inventory_raw),
@@ -52,6 +52,7 @@ from yuqei_ai_platform_api.provider_review_package import ( # noqa: E402
ProviderReviewCase,
ProviderTechnicalReviewPackage,
canonical_json,
canonical_review_asset_bytes,
canonical_review_package,
review_package_sha256,
sha256_bytes,
@@ -627,7 +628,7 @@ def _manifest_hash(paths: list[Path]) -> str:
for path in paths:
digest.update(path.name.encode("utf-8"))
digest.update(b"\0")
digest.update(path.read_bytes())
digest.update(canonical_review_asset_bytes(path))
digest.update(b"\0")
return digest.hexdigest()
@@ -683,7 +684,7 @@ def _review_assets() -> list[ProviderReviewAsset]:
assets: list[ProviderReviewAsset] = []
for path in sorted(item for item in PHASE13_ROOT.rglob("*") if item.is_file()):
relative = path.relative_to(PHASE13_ROOT).as_posix()
raw = path.read_bytes()
raw = canonical_review_asset_bytes(path)
assets.append(
ProviderReviewAsset(path=relative, sha256=sha256_bytes(raw), size_bytes=len(raw))
)
@@ -42,6 +42,7 @@ from yuqei_ai_platform_api.provider_review_package import ( # noqa: E402
ProviderReviewCase,
ProviderTechnicalReviewPackage,
canonical_json,
canonical_review_asset_bytes,
canonical_review_package,
sha256_bytes,
)
@@ -61,7 +62,7 @@ def _review_package() -> ProviderTechnicalReviewPackage:
inventory = expected_acceptance_inventory()
assets = []
for path in sorted(item for item in helper.PHASE13_ROOT.rglob("*") if item.is_file()):
raw = path.read_bytes()
raw = canonical_review_asset_bytes(path)
assets.append(
ProviderReviewAsset(
path=path.relative_to(helper.PHASE13_ROOT).as_posix(),
@@ -36,7 +36,24 @@ from yuqei_ai_platform_api.provider_legal_approval import ( # noqa: E402
ProviderLegalApprovalPublicKey,
sign_legal_approval,
)
from yuqei_ai_platform_api.provider_review_package import review_package_sha256 # noqa: E402
from yuqei_ai_platform_api.provider_review_package import ( # noqa: E402
canonical_review_asset_bytes,
review_package_sha256,
)
def test_review_asset_and_manifest_hashes_ignore_checkout_line_endings(tmp_path: Path) -> None:
lf_dir = tmp_path / "lf"
crlf_dir = tmp_path / "crlf"
lf_dir.mkdir()
crlf_dir.mkdir()
lf_path = lf_dir / "inventory.csv"
crlf_path = crlf_dir / "inventory.csv"
lf_path.write_bytes(b"id,title\n1,alpha\n2,beta\n")
crlf_path.write_bytes(b"id,title\r\n1,alpha\r\n2,beta\r\n")
assert canonical_review_asset_bytes(lf_path) == canonical_review_asset_bytes(crlf_path)
assert cli._manifest_hash([lf_path]) == cli._manifest_hash([crlf_path])
def _technical_package():