build(agent): jabba#56a767 iteration
This commit is contained in:
parent
2fa9850cea
commit
c6336b1edd
|
|
@ -20,6 +20,10 @@ New modules added in this iteration:
|
||||||
probability-of-violation (PoV) and an entropy summary. Includes serialize()
|
probability-of-violation (PoV) and an entropy summary. Includes serialize()
|
||||||
for compact fingerprints. Unit tests in tests/test_belief.py exercise
|
for compact fingerprints. Unit tests in tests/test_belief.py exercise
|
||||||
deterministic behavior and compact serialization.
|
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
|
Testing
|
||||||
- Run `./test.sh` to execute unit tests and build a sdist/wheel with `python3 -m build`.
|
- Run `./test.sh` to execute unit tests and build a sdist/wheel with `python3 -m build`.
|
||||||
|
|
|
||||||
|
|
@ -9,5 +9,9 @@ What is included in this change
|
||||||
- Unit tests under `tests/` exercising contract evaluation and engine behavior
|
- Unit tests under `tests/` exercising contract evaluation and engine behavior
|
||||||
- Packaging metadata (`pyproject.toml`, `setup.cfg`) so `python3 -m build` works
|
- Packaging metadata (`pyproject.toml`, `setup.cfg`) so `python3 -m build` works
|
||||||
- `test.sh` which runs the test-suite and builds a sdist/wheel
|
- `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.
|
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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
Loading…
Reference in New Issue