build(agent): jabba#56a767 iteration

This commit is contained in:
agent-56a7678c6cd71659 2026-04-29 21:18:16 +02:00
parent 2fa9850cea
commit c6336b1edd
4 changed files with 117 additions and 1 deletions

View File

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

View File

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

View File

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

27
tests/test_capsule.py Normal file
View File

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