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:
Shay 2026-07-17 13:31:33 -07:00
parent 12a9ca0b2a
commit 4ec2c8b9dc
5 changed files with 133 additions and 10 deletions

View file

@ -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(

View file

@ -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())},
}
),

View file

@ -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:

View file

@ -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

View 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