From c6336b1edd91a4014f748896cc5c077faed76adc Mon Sep 17 00:00:00 2001 From: agent-56a7678c6cd71659 Date: Wed, 29 Apr 2026 21:18:16 +0200 Subject: [PATCH] build(agent): jabba#56a767 iteration --- AGENTS.md | 4 ++ README.md | 6 ++- src/guardrail_space/capsule.py | 81 ++++++++++++++++++++++++++++++++++ tests/test_capsule.py | 27 ++++++++++++ 4 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 src/guardrail_space/capsule.py create mode 100644 tests/test_capsule.py diff --git a/AGENTS.md b/AGENTS.md index 694d2d4..5b12fee 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,6 +20,10 @@ New modules added in this iteration: probability-of-violation (PoV) and an entropy summary. Includes serialize() for compact fingerprints. Unit tests in tests/test_belief.py exercise deterministic behavior and compact serialization. + - src/guardrail_space/capsule.py: Poetic Situation Capsule generator that + emits a tiny 128-512B human+machine readable capsule (round,plan_hash,PoV, + energy_gap,verdict,human_summary). Tests in tests/test_capsule.py cover + deterministic generation and size constraints. Testing - Run `./test.sh` to execute unit tests and build a sdist/wheel with `python3 -m build`. diff --git a/README.md b/README.md index 448c22c..84579a3 100644 --- a/README.md +++ b/README.md @@ -9,5 +9,9 @@ What is included in this change - Unit tests under `tests/` exercising contract evaluation and engine behavior - Packaging metadata (`pyproject.toml`, `setup.cfg`) so `python3 -m build` works - `test.sh` which runs the test-suite and builds a sdist/wheel - This is a focused increment toward the full GuardRail.Space vision (WASM runner, ROS shims, fuzz harness, etc.). Next steps can extend the DSL, add serialization (YAML/DSL), shadow runner integration, and the situation-summary artifacts. + +Added in this iteration: +- `src/guardrail_space/capsule.py`: Poetic Situation Capsule generator for tiny + (128-512B) human+machine readable summaries useful in low-bandwidth operator + triage. See tests/test_capsule.py for deterministic behavior and size checks. diff --git a/src/guardrail_space/capsule.py b/src/guardrail_space/capsule.py new file mode 100644 index 0000000..1b47670 --- /dev/null +++ b/src/guardrail_space/capsule.py @@ -0,0 +1,81 @@ +import json +import hashlib +from typing import Optional + + +def _choose_template(plan_hash: str) -> str: + # Deterministically pick one of a few brief templates based on hash + templates = [ + "Round {round}: plan {plan} — PoV {pov:.2f}, gap {gap:.2f} — verdict: {verdict}.", + "Capsule: r{round} {plan} | PoV={pov:.2f} gap={gap:.2f} -> {verdict}", + "[{verdict}] plan={plan} r={round} pov={pov:.2f} gap={gap:.2f}", + ] + h = int(hashlib.sha256(plan_hash.encode("utf-8")).hexdigest(), 16) + return templates[h % len(templates)] + + +def make_capsule( + plan_hash: str, + pov: float, + energy_gap: float, + verdict_code: str, + round_number: int = 0, + human_summary: Optional[str] = None, + min_size: int = 128, + max_size: int = 512, +) -> bytes: + """Create a compact Situation Capsule (machine + human readable). + + Returns a deterministic byte blob between min_size and max_size bytes. + + Fields included (compact): + - round (int), plan_hash (short), pov (float), energy_gap (float), verdict (str) + - human (1-2 sentence summary). If human_summary is not provided a small + template is chosen deterministically from the plan_hash. + + The output is a UTF-8 JSON object. To ensure a compact fixed-size blob we + trim or pad the human summary. Padding uses spaces so the capsule remains + human-readable when printed. + """ + short_hash = hashlib.sha256(plan_hash.encode("utf-8")).hexdigest()[:12] + + if human_summary is None: + tpl = _choose_template(plan_hash) + human_summary = tpl.format(round=round_number, plan=short_hash, pov=pov, gap=energy_gap, verdict=verdict_code) + + # Ensure human summary is a single short sentence (truncate if necessary) + human_summary = " ".join(human_summary.strip().split()) + + payload = { + "r": int(round_number), + "p": short_hash, + "v": round(float(pov), 4), + "g": round(float(energy_gap), 4), + "c": verdict_code, + "h": human_summary, + } + + blob = json.dumps(payload, separators=(",", ":") , ensure_ascii=False).encode("utf-8") + + # If too long, truncate human summary and re-encode until fits + if len(blob) > max_size: + # remove human summary progressively + h = payload["h"] + while len(blob) > max_size and len(h) > 0: + h = h[:-1] + payload["h"] = h + blob = json.dumps(payload, separators=(",", ":") , ensure_ascii=False).encode("utf-8") + + # If too short, pad the human summary to reach min_size (but never exceed max_size) + if len(blob) < min_size: + pad_needed = min_size - len(blob) + # simple space padding appended to human summary (keeps it readable) + h = payload["h"] + (" " * pad_needed) + payload["h"] = h + blob = json.dumps(payload, separators=(",", ":") , ensure_ascii=False).encode("utf-8") + + # Final safety: if still out of bounds, clamp to max_size by truncating bytes + if len(blob) > max_size: + blob = blob[:max_size] + + return blob diff --git a/tests/test_capsule.py b/tests/test_capsule.py new file mode 100644 index 0000000..bec661b --- /dev/null +++ b/tests/test_capsule.py @@ -0,0 +1,27 @@ +from guardrail_space.capsule import make_capsule +import json + + +def test_capsule_size_and_fields_default(): + blob = make_capsule(plan_hash="plan-123", pov=0.75, energy_gap=1.5, verdict_code="warn", round_number=3) + assert isinstance(blob, (bytes, bytearray)) + assert 128 <= len(blob) <= 512 + + j = json.loads(blob.decode("utf-8", errors="ignore")) + # machine fields + assert j.get("r") == 3 + assert "p" in j and isinstance(j["p"], str) + assert abs(j.get("v") - 0.75) < 1e-6 + assert abs(j.get("g") - 1.5) < 1e-6 + assert j.get("c") == "warn" + # human summary exists + assert isinstance(j.get("h"), str) and len(j.get("h")) > 0 + + +def test_capsule_user_summary_truncation_and_determinism(): + # long user-provided summary should be truncated/padded deterministically + long_summary = "This is a very long human summary. " * 20 + b1 = make_capsule(plan_hash="plan-abc", pov=0.1, energy_gap=0.0, verdict_code="ok", human_summary=long_summary) + b2 = make_capsule(plan_hash="plan-abc", pov=0.1, energy_gap=0.0, verdict_code="ok", human_summary=long_summary) + assert b1 == b2 + assert 128 <= len(b1) <= 512