diff --git a/src/idea194_attractorforge_verified_attractor/models.py b/src/idea194_attractorforge_verified_attractor/models.py index f2ae756..d600ec5 100644 --- a/src/idea194_attractorforge_verified_attractor/models.py +++ b/src/idea194_attractorforge_verified_attractor/models.py @@ -68,6 +68,33 @@ class BasinSketch: attractor_fingerprint: str dt: float + def compact_payload(self) -> bytes: + """Return a compact, deterministic payload suitable for DTN exchange. + + The payload is a minified JSON of a small set of stable fields. If the + resulting UTF-8 encoding exceeds 128 bytes, return a SHA-256 hex digest + (64 ASCII chars) of that JSON instead. This guarantees the payload is + deterministic and bounded in size. + """ + import json + import hashlib + + payload = { + "id": self.attractor_id, + "kind": self.kind, + "radius": _canonical_float(self.estimated_basin_radius), + "conv_rate": _canonical_float(self.convergence_rate_bound), + "ctrl_hash": self.controller_hash, + "fp": self.attractor_fingerprint, + "dt": _canonical_float(self.dt), + } + j = json.dumps(payload, sort_keys=True, separators=(",", ":")) + b = j.encode("utf-8") + if len(b) <= 128: + return b + # fallback to a stable SHA-256 hex representation (64 bytes ASCII) + return hashlib.sha256(j.encode("utf-8")).hexdigest().encode("ascii") + @dataclass(frozen=True, slots=True) class PlanCert: diff --git a/tests/test_basin_sketch.py b/tests/test_basin_sketch.py new file mode 100644 index 0000000..d095faa --- /dev/null +++ b/tests/test_basin_sketch.py @@ -0,0 +1,26 @@ +from idea194_attractorforge_verified_attractor.models import BasinSketch + + +def test_basin_sketch_compact_payload_is_bounded_and_deterministic() -> None: + sketch = BasinSketch( + attractor_id="demo-1", + kind="motion_manifold", + estimated_basin_radius=1.23456789, + convergence_rate_bound=0.3456789, + controller_hash="deadbeef" * 8, + attractor_fingerprint="f" * 64, + dt=0.1, + ) + + p1 = sketch.compact_payload() + p2 = sketch.compact_payload() + + # deterministic + assert p1 == p2 + + # size bound: either the inline JSON (<=128) or the 64-char hex digest + assert len(p1) <= 128 + + # if JSON fits, it must start with a brace + if p1.startswith(b"{"): + assert b'"id":"demo-1"' in p1