refactor(adr-0244): D1 semantic rigor — full 256-bit digests + LE byte-order
Cohesion directive Mandates 4+5. The ADR-0243 lifecycle modules were the only content-address sites still truncating SHA-256 to 96 bits (24 hex) and passing default=str to json.dumps — the rest of the codebase already uses full 64-hex digests (claim_digest, cert_id, decision_id, pressure_id, ...). The default=str + [:24] drift had also just replicated into Lane C (biography_wiring.py). - cognitive_lifecycle.py: _content_id drops default=str (non-serializable payload now fails closed with TypeError) + full 64-hex; _psi_digest full 64-hex over canonical little-endian f64 bytes via a new _le_f64_bytes helper (coercion, not the assert form the ADR-0244 draft used — assert is stripped under -O); matrix_sha routed through _le_f64_bytes too. - biography_wiring.py, self_authorship.py: same _content_id hardening. Verified every payload is JSON-safe after the existing float()/str() wrapping, so removing default=str changes no current digest value — only future non-serializable inputs fail closed. - On little-endian targets the widening is an un-truncation: the new 64-hex digest's 24-char prefix equals the old value (pinned). tests: test_adr_0243_cognitive_lifecycle psi_digest gold updated to full+LE; new test_adr_0244_semantic_rigor pins full-64 length, fail-closed TypeError, LE byte-order canonicality (native vs >f8 hash-equal), and determinism. [Verification]: D1-affected suites 66 + miner suites 23 green; smoke + fast lane below.
This commit is contained in:
parent
12a9ca0b2a
commit
4ec2c8b9dc
5 changed files with 133 additions and 10 deletions
|
|
@ -122,8 +122,10 @@ class BiographyProvenanceRecord:
|
|||
|
||||
|
||||
def _content_id(payload: Mapping[str, Any]) -> str:
|
||||
raw = json.dumps(payload, sort_keys=True, separators=(",", ":"), default=str)
|
||||
return hashlib.sha256(raw.encode("utf-8")).hexdigest()[:24]
|
||||
# Full 256-bit digest (64 hex); no ``default=str`` — non-serializable
|
||||
# payload elements fail closed with a typed ``TypeError`` (ADR-0244 §2.7).
|
||||
raw = json.dumps(payload, sort_keys=True, separators=(",", ":"))
|
||||
return hashlib.sha256(raw.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def biography_provenance_record(
|
||||
|
|
|
|||
|
|
@ -124,13 +124,33 @@ class EgressValidationError(CognitiveLifecycleError):
|
|||
# --- Content addressing ----------------------------------------------------------
|
||||
|
||||
|
||||
def _le_f64_bytes(arr: np.ndarray) -> bytes:
|
||||
"""Canonical little-endian float64 bytes for cross-platform-stable digests.
|
||||
|
||||
ADR-0244 §2.7 byte-order guard: coerce to little-endian float64 before
|
||||
hashing so the digest is identical on every little-endian platform and
|
||||
deterministic on big-endian ones. Byte-wise a no-op on the M1/x86 targets,
|
||||
but an explicit contract rather than an implicit platform assumption — and a
|
||||
coercion, not an ``assert`` (the assert form is stripped under ``-O``).
|
||||
"""
|
||||
contiguous = np.ascontiguousarray(arr, dtype=np.float64)
|
||||
return contiguous.astype(np.dtype("<f8"), copy=False).tobytes()
|
||||
|
||||
|
||||
def _content_id(payload: Mapping[str, Any]) -> str:
|
||||
raw = json.dumps(payload, sort_keys=True, separators=(",", ":"), default=str)
|
||||
return hashlib.sha256(raw.encode("utf-8")).hexdigest()[:24]
|
||||
# Full 256-bit digest (64 hex). No ``default=str``: a non-serializable
|
||||
# payload element fails closed with a typed ``TypeError`` at the
|
||||
# serialization boundary rather than silently collapsing distinct objects
|
||||
# onto identical string forms (ADR-0244 §2.7).
|
||||
raw = json.dumps(payload, sort_keys=True, separators=(",", ":"))
|
||||
return hashlib.sha256(raw.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def _psi_digest(psi: np.ndarray) -> str:
|
||||
return hashlib.sha256(np.ascontiguousarray(psi, dtype=np.float64).tobytes()).hexdigest()[:24]
|
||||
# Full 256-bit digest over canonical little-endian float64 bytes — no
|
||||
# 96-bit truncation (birthday-collision floor at 2^48), no platform
|
||||
# byte-order ambiguity (ADR-0244 §2.7).
|
||||
return hashlib.sha256(_le_f64_bytes(psi)).hexdigest()
|
||||
|
||||
|
||||
def _as_psi(x: np.ndarray, name: str, *, error: type[CognitiveLifecycleError]) -> np.ndarray:
|
||||
|
|
@ -271,7 +291,7 @@ class ProblemHamiltonian:
|
|||
_content_id(
|
||||
{
|
||||
"domain": str(self.domain),
|
||||
"matrix_sha": hashlib.sha256(arr.tobytes()).hexdigest(),
|
||||
"matrix_sha": hashlib.sha256(_le_f64_bytes(arr)).hexdigest(),
|
||||
"metadata": {k: str(v) for k, v in sorted(meta.items())},
|
||||
}
|
||||
),
|
||||
|
|
|
|||
|
|
@ -39,8 +39,10 @@ class AuthorshipProposal:
|
|||
|
||||
|
||||
def _content_id(payload: Mapping[str, Any]) -> str:
|
||||
raw = json.dumps(payload, sort_keys=True, separators=(",", ":"), default=str)
|
||||
return hashlib.sha256(raw.encode("utf-8")).hexdigest()[:24]
|
||||
# Full 256-bit digest (64 hex); no ``default=str`` — non-serializable
|
||||
# payload elements fail closed with a typed ``TypeError`` (ADR-0244 §2.7).
|
||||
raw = json.dumps(payload, sort_keys=True, separators=(",", ":"))
|
||||
return hashlib.sha256(raw.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
class SelfAuthorshipMiner:
|
||||
|
|
|
|||
|
|
@ -569,8 +569,8 @@ def test_egress_refuses_certificate_not_bound_to_state():
|
|||
cert = outcome.relaxation.certificate
|
||||
# Independent gold for the binding: byte digest of the certified state.
|
||||
gold_digest = hashlib.sha256(
|
||||
np.ascontiguousarray(psi, dtype=np.float64).tobytes()
|
||||
).hexdigest()[:24]
|
||||
np.ascontiguousarray(psi, dtype=np.float64).astype(np.dtype("<f8"), copy=False).tobytes()
|
||||
).hexdigest()
|
||||
assert cert.psi_digest == gold_digest
|
||||
assert cert.as_dict()["psi_digest"] == gold_digest
|
||||
|
||||
|
|
|
|||
99
tests/test_adr_0244_semantic_rigor.py
Normal file
99
tests/test_adr_0244_semantic_rigor.py
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
"""ADR-0244 §2.7 semantic-rigor pins (cohesion directive Mandates 4 + 5).
|
||||
|
||||
Content-address keys must (1) retain the full 256-bit SHA-256 digest — no
|
||||
96-bit (24-hex) truncation, which floors a birthday collision at 2^48 and can
|
||||
corrupt content-addressed merge keys; (2) fail closed on non-serializable
|
||||
payload elements rather than silently coercing them via ``default=str``; and
|
||||
(3) hash a canonical little-endian float64 byte layout so digests are identical
|
||||
across little-endian platforms and deterministic on big-endian ones.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from algebra.rotor import make_rotor_from_angle
|
||||
from core.physics.cognitive_lifecycle import (
|
||||
_content_id as lifecycle_content_id,
|
||||
_le_f64_bytes,
|
||||
_psi_digest,
|
||||
)
|
||||
from core.physics.biography_wiring import _content_id as biography_content_id
|
||||
from core.physics.self_authorship import _content_id as authorship_content_id
|
||||
|
||||
_CONTENT_ID_FUNCS = (
|
||||
lifecycle_content_id,
|
||||
biography_content_id,
|
||||
authorship_content_id,
|
||||
)
|
||||
|
||||
|
||||
def _sample_psi() -> np.ndarray:
|
||||
return np.ascontiguousarray(make_rotor_from_angle(0.7, 6), dtype=np.float64)
|
||||
|
||||
|
||||
# --- Mandate 4: full 256-bit digests (no 96-bit truncation) -----------------------
|
||||
|
||||
|
||||
@pytest.mark.parametrize("content_id", _CONTENT_ID_FUNCS)
|
||||
def test_content_id_is_full_256_bit_hex(content_id) -> None:
|
||||
digest = content_id({"a": 1, "b": ["x", 2.0], "c": True})
|
||||
assert len(digest) == 64
|
||||
int(digest, 16) # valid lowercase hex
|
||||
|
||||
|
||||
def test_psi_digest_is_full_256_bit_hex() -> None:
|
||||
digest = _psi_digest(_sample_psi())
|
||||
assert len(digest) == 64
|
||||
int(digest, 16)
|
||||
|
||||
|
||||
def test_full_digest_extends_the_old_truncated_prefix() -> None:
|
||||
"""The widening is an un-truncation, not a different hash: the old 24-hex
|
||||
form is exactly the 64-hex digest's prefix (same underlying bytes)."""
|
||||
psi = _sample_psi()
|
||||
full = _psi_digest(psi)
|
||||
legacy_prefix = hashlib.sha256(_le_f64_bytes(psi)).hexdigest()[:24]
|
||||
assert full[:24] == legacy_prefix
|
||||
|
||||
|
||||
# --- Mandate 4: fail closed on silent coercion ------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.parametrize("content_id", _CONTENT_ID_FUNCS)
|
||||
def test_content_id_fails_closed_on_non_serializable(content_id) -> None:
|
||||
class _Opaque:
|
||||
pass
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
content_id({"bad": _Opaque()})
|
||||
|
||||
|
||||
@pytest.mark.parametrize("content_id", _CONTENT_ID_FUNCS)
|
||||
def test_content_id_is_deterministic(content_id) -> None:
|
||||
payload = {"k": "v", "n": 3, "xs": [1, 2, 3]}
|
||||
assert content_id(payload) == content_id(dict(payload))
|
||||
|
||||
|
||||
# --- Mandate 5: canonical little-endian byte-order contract -----------------------
|
||||
|
||||
|
||||
def test_le_bytes_are_byte_order_canonical() -> None:
|
||||
"""A native-endian array and an explicit big-endian-dtype array carrying the
|
||||
same values must hash identically — the coercion, not the platform, decides
|
||||
the bytes."""
|
||||
psi = _sample_psi()
|
||||
native = np.ascontiguousarray(psi, dtype=np.float64)
|
||||
big_endian = native.astype(np.dtype(">f8"))
|
||||
assert _le_f64_bytes(native) == _le_f64_bytes(big_endian)
|
||||
assert _psi_digest(native) == _psi_digest(big_endian)
|
||||
|
||||
|
||||
def test_le_bytes_match_explicit_little_endian_layout() -> None:
|
||||
psi = _sample_psi()
|
||||
expected = np.ascontiguousarray(psi, dtype=np.dtype("<f8")).tobytes()
|
||||
assert _le_f64_bytes(psi) == expected
|
||||
assert len(_le_f64_bytes(psi)) == 32 * 8 # 32 float64 components
|
||||
Loading…
Reference in a new issue