diff --git a/core/contemplation/miners/articulation_quality.py b/core/contemplation/miners/articulation_quality.py index 7c7aafe6..66af2d71 100644 --- a/core/contemplation/miners/articulation_quality.py +++ b/core/contemplation/miners/articulation_quality.py @@ -297,13 +297,16 @@ def _substrate_hash_for_observations( ) -> str: """Deterministic hash over the canonical concatenation of each observation's JSONL serialisation.""" + # No ``default=str``: ``ArticulationObservation.as_dict()`` is JSON-native + # (ints, str, python-float ratios, None), so a non-serializable field fails + # closed rather than silently str()-collapsing distinct objects onto one id. + # Full 256-bit digest — no 16-hex truncation (ADR-0244 §2.7 / ADR-0245 §2.3). payload = json.dumps( [obs.as_dict() for obs in observations], sort_keys=True, separators=(",", ":"), - default=str, ) - return hashlib.sha256(payload.encode("utf-8")).hexdigest()[:16] + return hashlib.sha256(payload.encode("utf-8")).hexdigest() def mine_articulation_observations( diff --git a/core/contemplation/plan_preflight.py b/core/contemplation/plan_preflight.py index 69f1c03c..76d09c78 100644 --- a/core/contemplation/plan_preflight.py +++ b/core/contemplation/plan_preflight.py @@ -73,13 +73,15 @@ twice for two domain memberships). Three or more turns mechanical. def _plan_substrate_hash(plan: DiscoursePlan) -> str: - """SHA-256-16 of the plan's canonical JSON. + """Full SHA-256 (64 hex) of the plan's canonical JSON. Used as the ``substrate_hash`` on every emitted finding so two contemplation passes over byte-equal plans produce byte-equal - finding IDs. + finding IDs. Full digest — no 16-hex truncation (ADR-0244 §2.7 / + ADR-0245 §2.3): the substrate hash content-addresses the finding, so a + truncated digest would floor collision resistance. """ - return hashlib.sha256(plan.to_json().encode("utf-8")).hexdigest()[:16] + return hashlib.sha256(plan.to_json().encode("utf-8")).hexdigest() def _evidence_ref_for_plan(plan: DiscoursePlan) -> ContemplationEvidenceRef: diff --git a/core/contemplation/schema.py b/core/contemplation/schema.py index 42efc2f2..434f7447 100644 --- a/core/contemplation/schema.py +++ b/core/contemplation/schema.py @@ -38,8 +38,13 @@ def _canonical_json(payload: dict[str, Any]) -> str: ) -def _sha256_16(payload: dict[str, Any]) -> str: - return hashlib.sha256(_canonical_json(payload).encode("utf-8")).hexdigest()[:16] +def _content_digest(payload: dict[str, Any]) -> str: + # Full 256-bit digest (64 hex) over canonical JSON — no 16-hex truncation + # (which floored birthday-collision resistance at 2^32). ADR-0244 §2.7 / + # ADR-0245 §2.3 semantic rigor: a content-address must not collide two + # distinct findings into the same id. ``_canonical_json`` already forbids + # ``default=str``, so a non-serializable payload fails closed here. + return hashlib.sha256(_canonical_json(payload).encode("utf-8")).hexdigest() # BOUNDARY (vs teaching/discovery.py:EvidencePointer) @@ -134,7 +139,7 @@ class ContemplationFinding: if not self.evidence_refs: raise ValueError("ContemplationFinding requires at least one evidence ref") if not self.finding_id: - object.__setattr__(self, "finding_id", _sha256_16(self._identity_dict())) + object.__setattr__(self, "finding_id", _content_digest(self._identity_dict())) def _identity_dict(self) -> dict[str, Any]: return { @@ -194,7 +199,7 @@ class ContemplationRun: if finding.epistemic_status is not EpistemicStatus.SPECULATIVE: raise ValueError("ContemplationRun cannot contain non-SPECULATIVE findings") if not self.run_id: - object.__setattr__(self, "run_id", _sha256_16(self._identity_dict())) + object.__setattr__(self, "run_id", _content_digest(self._identity_dict())) def _identity_dict(self) -> dict[str, Any]: return { diff --git a/core/physics/holographic_vault.py b/core/physics/holographic_vault.py index 8f377846..356e0bbd 100644 --- a/core/physics/holographic_vault.py +++ b/core/physics/holographic_vault.py @@ -71,8 +71,13 @@ def _as_mv(psi: np.ndarray, name: str = "ψ") -> np.ndarray: def _default_mode_id(psi: np.ndarray) -> str: - digest = hashlib.sha256(np.asarray(psi, dtype=np.float64).tobytes()).hexdigest() - return f"mode-{digest[:16]}" + # Full 256-bit digest over canonical little-endian float64 bytes (ADR-0244 + # §2.7 / ADR-0245 §2.3): no 16-hex truncation (which floored the durable + # mode-id's collision resistance at 2^32) and an explicit LE byte-order + # coercion so the id is platform-stable, not an implicit host-endianness + # assumption via bare ``.tobytes()``. + le_bytes = np.ascontiguousarray(psi, dtype=np.dtype("