[Vincent] #19: Executable canon lifecycle validator with daily receipt fixtures #27

Closed
vincent wants to merge 2 commits from vincent/19-daily-canon-receipts into main
10 changed files with 506 additions and 2 deletions
Showing only changes of commit b8d5e686c2 - Show all commits

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
__pycache__/
*.pyc
*.pyo
.pytest_cache/

1
projects/__init__.py Normal file
View File

@ -0,0 +1 @@
"""Projects package."""

View File

@ -0,0 +1,6 @@
"""Cannon Chain project."""
import sys
from pathlib import Path
# Allow importing slop_cannon modules from the project root
sys.path.insert(0, str(Path(__file__).parents[2]))

View File

@ -0,0 +1,228 @@
"""
Dependency-free JSON Schema validator for Cannon Chain canon lifecycle.
Validates:
- form.state transitions (candidate -> survivor | archive_ghost)
- audience_choice schema (survivor_id, stolen_trait, source_episode, source_issue, committed_at)
- receipt schema (episode_id, form_id, provenance, signed_by, deltas)
- signer allowlist (agent:vincent, agent:timmy, human:*)
- Exactly one survivor per episode
- Rejected forms promoted to archive_ghost
- Illegal state transitions rejected
Usage:
from projects.slop_cannon.canon_validator import validate_canon
result = validate_canon(canon_data)
assert result.valid, result.errors
"""
import json
import re
from pathlib import Path
from datetime import datetime
CANON_PATH = Path(__file__).parent / "canon.json"
VALID_STATES = {"candidate", "survivor", "archive_ghost"}
VALID_TRANSITIONS = {
"candidate": {"survivor", "archive_ghost"},
"survivor": set(), # terminal
"archive_ghost": set(), # terminal
}
SIGNER_PATTERN = re.compile(r"^(agent:(?:vincent|timmy)|human:\w+)$")
def validate_canon(data: dict) -> dict:
"""
Validate canon.json data against lifecycle schemas.
Returns:
{"valid": bool, "errors": list[str], "warnings": list[str]}
"""
errors = []
warnings = []
# --- Top-level structure ---
if "schema_version" not in data:
errors.append("Missing schema_version")
if "forms" not in data:
errors.append("Missing forms array")
return {"valid": False, "errors": errors, "warnings": warnings}
forms = data["forms"]
if not isinstance(forms, list) or len(forms) == 0:
errors.append("forms must be a non-empty array")
return {"valid": False, "errors": errors, "warnings": warnings}
form_ids = set()
survivor_count = 0
form_id_map = {}
for i, form in enumerate(forms):
prefix = f"forms[{i}]"
if "id" not in form:
errors.append(f"{prefix}: missing id")
continue
fid = form["id"]
form_id_map[fid] = i
if fid in form_ids:
errors.append(f"{prefix}: duplicate form id '{fid}'")
form_ids.add(fid)
# Required fields
for field in ["power", "scar", "cost"]:
if field not in form or not form[field]:
errors.append(f"{prefix}: missing or empty '{field}'")
# Palette
if "palette" not in form or not isinstance(form["palette"], list):
errors.append(f"{prefix}: missing or invalid palette")
elif len(form["palette"]) < 3:
errors.append(f"{prefix}: palette must have at least 3 colors, got {len(form['palette'])}")
# Lifecycle state
if "state" not in form:
errors.append(f"{prefix}: missing lifecycle state")
elif form["state"] not in VALID_STATES:
errors.append(f"{prefix}: invalid state '{form['state']}', expected one of {VALID_STATES}")
else:
if form["state"] == "survivor":
survivor_count += 1
# Exactly one survivor (or zero if no choice yet)
if survivor_count > 1:
errors.append(f"Too many survivors: {survivor_count} (expected 0 or 1)")
# --- Episodes ---
if "episodes" in data:
episodes = data["episodes"]
ready_count = sum(1 for ep in episodes if ep.get("state") == "ready")
if ready_count > 1:
errors.append(f"Too many ready episodes: {ready_count} (expected at most 1)")
# Check ordering: first ready, rest locked
found_ready = False
for ep in episodes:
if ep.get("state") == "ready":
if found_ready:
errors.append(f"Ready episode '{ep['id']}' after another ready episode")
found_ready = True
elif ep.get("state") != "locked":
errors.append(f"Episode '{ep['id']}' has invalid state '{ep.get('state')}'")
# --- Audience choice schema ---
if data.get("audience_choice") is not None:
choice = data["audience_choice"]
for field in ["survivor_id", "stolen_trait", "source_episode", "source_issue", "committed_at"]:
if field not in choice:
errors.append(f"audience_choice: missing '{field}'")
# survivor_id must reference a valid form
if "survivor_id" in choice:
if choice["survivor_id"] not in form_id_map:
errors.append(f"audience_choice: survivor_id '{choice['survivor_id']}' not found in forms")
# committed_at must be valid ISO timestamp
if "committed_at" in choice:
try:
datetime.fromisoformat(choice["committed_at"])
except (ValueError, TypeError):
errors.append(f"audience_choice: committed_at is not a valid ISO timestamp")
# If choice is made, exactly one survivor required
if survivor_count == 0:
errors.append("audience_choice is set but no survivor found in forms")
# --- Receipts ---
if "receipts" in data:
for i, receipt in enumerate(data["receipts"]):
prefix = f"receipts[{i}]"
for field in ["episode_id", "form_id", "provenance", "signed_by"]:
if field not in receipt or not receipt[field]:
errors.append(f"{prefix}: missing or empty '{field}'")
# Signed-by allowlist
if "signed_by" in receipt:
if not SIGNER_PATTERN.match(receipt["signed_by"]):
errors.append(
f"{prefix}: invalid signed_by '{receipt['signed_by']}', "
f"expected pattern: {SIGNER_PATTERN.pattern}"
)
# Form_id must reference a valid form
if "form_id" in receipt:
if receipt["form_id"] not in form_id_map:
errors.append(f"{prefix}: form_id '{receipt['form_id']}' not found in forms")
# Deltas
for delta in ["power_delta", "scar_delta", "cost_delta"]:
if delta in receipt and not receipt[delta]:
warnings.append(f"{prefix}: {delta} is present but empty")
return {
"valid": len(errors) == 0,
"errors": errors,
"warnings": warnings,
}
def validate_transition(form: dict, new_state: str) -> dict:
"""
Validate a state transition for a single form.
Returns:
{"valid": bool, "errors": list[str]}
"""
errors = []
current = form.get("state")
if not current:
errors.append("Form has no current state")
return {"valid": False, "errors": errors}
if current not in VALID_TRANSITIONS:
errors.append(f"Invalid current state '{current}'")
return {"valid": False, "errors": errors}
if new_state not in VALID_STATES:
errors.append(f"Invalid target state '{new_state}', expected one of {VALID_STATES}")
return {"valid": False, "errors": errors}
if new_state not in VALID_TRANSITIONS.get(current, set()):
errors.append(
f"Illegal transition: '{current}' -> '{new_state}'"
)
return {"valid": False, "errors": errors}
return {"valid": True, "errors": []}
def load_canon(path: str = None) -> dict:
"""Load canon.json from the default path or a given path."""
target = Path(path) if path else CANON_PATH
return json.loads(target.read_text())
def check_canon(path: str = None) -> dict:
"""Load and validate canon.json. Convenience entry point."""
data = load_canon(path)
return validate_canon(data)
if __name__ == "__main__":
import sys
result = check_canon()
if result["valid"]:
print(f"canon.json: VALID")
if result["warnings"]:
for w in result["warnings"]:
print(f" WARNING: {w}")
else:
print(f"canon.json: INVALID")
for e in result["errors"]:
print(f" ERROR: {e}")
sys.exit(1)

View File

@ -3,7 +3,7 @@ import unittest
from pathlib import Path
CANON = Path(__file__).parents[1] / "projects" / "slop-cannon" / "canon.json"
CANON = Path(__file__).parents[1] / "projects" / "slop_cannon" / "canon.json"
class CannonCanonTests(unittest.TestCase):

View File

@ -2,7 +2,7 @@ import json
import unittest
from pathlib import Path
CANON = Path(__file__).parents[1] / "projects" / "slop-cannon" / "canon.json"
CANON = Path(__file__).parents[1] / "projects" / "slop_cannon" / "canon.json"
class CanonLifecycleTests(unittest.TestCase):

View File

@ -0,0 +1,265 @@
"""
Validator tests for canon lifecycle schemas.
Covers:
- Valid canon structure (from #13 fix)
- Negative tests: malformed receipts, illegal transitions
- Daily Lab receipt fixture with provenance binding
"""
import json
import sys
import unittest
from pathlib import Path
from copy import deepcopy
# Add project root to path for imports
sys.path.insert(0, str(Path(__file__).parents[1]))
CANON = Path(__file__).parents[1] / "projects" / "slop_cannon" / "canon.json"
from projects.slop_cannon.canon_validator import (
validate_canon,
validate_transition,
)
class CanonValidatorTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.data = json.loads(CANON.read_text())
# --- Positive tests ---
def test_current_canon_is_valid(self):
"""canon.json as committed should pass validation."""
result = validate_canon(self.data)
self.assertTrue(result["valid"], f"Errors: {result['errors']}")
def test_all_forms_have_valid_state(self):
"""Every form state must be in the valid set."""
result = validate_canon(self.data)
self.assertTrue(result["valid"], result["errors"])
# --- Negative: malformed receipts ---
def test_missing_signed_by_fails(self):
"""Receipt without signed_by is rejected."""
data = deepcopy(self.data)
data["receipts"] = [
{
"episode_id": "01-pilot",
"form_id": "wizard",
"provenance": "#13",
}
]
result = validate_canon(data)
self.assertFalse(result["valid"])
self.assertTrue(any("signed_by" in e for e in result["errors"]))
def test_invalid_signer_fails(self):
"""Signed_by must match agent: or human: pattern."""
data = deepcopy(self.data)
data["receipts"] = [
{
"episode_id": "01-pilot",
"form_id": "wizard",
"provenance": "#13",
"signed_by": "unknown:bot",
}
]
result = validate_canon(data)
self.assertFalse(result["valid"])
self.assertTrue(any("invalid signed_by" in e for e in result["errors"]))
def test_empty_provenance_fails(self):
"""Receipt with empty provenance is rejected."""
data = deepcopy(self.data)
data["receipts"] = [
{
"episode_id": "01-pilot",
"form_id": "wizard",
"provenance": "",
"signed_by": "agent:vincent",
}
]
result = validate_canon(data)
self.assertFalse(result["valid"])
self.assertTrue(any("provenance" in e for e in result["errors"]))
def test_form_id_must_exist(self):
"""Receipt referencing nonexistent form is rejected."""
data = deepcopy(self.data)
data["receipts"] = [
{
"episode_id": "01-pilot",
"form_id": "nonexistent",
"provenance": "#13",
"signed_by": "agent:vincent",
}
]
result = validate_canon(data)
self.assertFalse(result["valid"])
self.assertTrue(any("not found in forms" in e for e in result["errors"]))
# --- Negative: illegal state transitions ---
def test_survivor_cannot_transition(self):
"""Survivor state is terminal."""
form = {"id": "wizard", "state": "survivor"}
result = validate_transition(form, "archive_ghost")
self.assertFalse(result["valid"])
self.assertTrue(any("Illegal transition" in e for e in result["errors"]))
def test_ghost_cannot_transition(self):
"""Archive_ghost state is terminal."""
form = {"id": "wizard", "state": "archive_ghost"}
result = validate_transition(form, "candidate")
self.assertFalse(result["valid"])
self.assertTrue(any("Illegal transition" in e for e in result["errors"]))
def test_candidate_to_survivor_valid(self):
"""Candidate -> survivor is the expected happy path."""
form = {"id": "wizard", "state": "candidate"}
result = validate_transition(form, "survivor")
self.assertTrue(result["valid"])
def test_candidate_to_ghost_valid(self):
"""Candidate -> archive_ghost is valid (rejected form)."""
form = {"id": "wizard", "state": "candidate"}
result = validate_transition(form, "archive_ghost")
self.assertTrue(result["valid"])
# --- Negative: audience choice ---
def test_two_survivors_fails(self):
"""Only one survivor allowed."""
data = deepcopy(self.data)
for f in data["forms"][:2]:
f["state"] = "survivor"
result = validate_canon(data)
self.assertFalse(result["valid"])
self.assertTrue(any("survivor" in e.lower() for e in result["errors"]))
def test_choice_without_survivor_fails(self):
"""Setting audience_choice requires a survivor in forms."""
data = deepcopy(self.data)
data["audience_choice"] = {
"survivor_id": "wizard",
"stolen_trait": "Proves the execution path",
"source_episode": "01-pilot",
"source_issue": "7",
"committed_at": "2026-08-11T19:00:00Z",
}
# forms all stay candidate -> no survivor
result = validate_canon(data)
self.assertFalse(result["valid"])
self.assertTrue(any("survivor" in e.lower() for e in result["errors"]))
def test_invalid_committed_at_fails(self):
"""committed_at must be valid ISO timestamp."""
data = deepcopy(self.data)
data["forms"][0]["state"] = "survivor" # satisfy survivor req
data["audience_choice"] = {
"survivor_id": "wizard",
"stolen_trait": "Negotiates visible covenants",
"source_episode": "01-pilot",
"source_issue": "7",
"committed_at": "not-a-date",
}
result = validate_canon(data)
self.assertFalse(result["valid"])
self.assertTrue(any("ISO" in e for e in result["errors"]))
# --- Negative: structure ---
def test_duplicate_form_id_fails(self):
"""Duplicate form IDs are rejected."""
data = deepcopy(self.data)
data["forms"].append(deepcopy(data["forms"][0]))
result = validate_canon(data)
self.assertFalse(result["valid"])
self.assertTrue(any("duplicate" in e for e in result["errors"]))
def test_short_palette_fails(self):
"""Palette needs at least 3 colors."""
data = deepcopy(self.data)
data["forms"][0]["palette"] = ["red"]
result = validate_canon(data)
self.assertFalse(result["valid"])
self.assertTrue(any("palette" in e for e in result["errors"]))
class DailyReceiptFixtureTests(unittest.TestCase):
"""Test the Daily Lab receipt fixture with Vincent's provenance."""
def test_vincent_receipt_fixture(self):
"""A valid Vincent contribution receipt passes validation."""
data = deepcopy(json.loads(CANON.read_text()))
# Build a receipt for the #13 contribution
data["receipts"] = [
{
"episode_id": "01-pilot",
"form_id": "wizard",
"action": "mutation",
"power_delta": "Added lifecycle state tracking",
"scar_delta": "Receipt schema now enforces provenance",
"cost_delta": "All future drops require signed receipts",
"provenance": "pull/18@562d0cf",
"signed_by": "agent:vincent",
}
]
result = validate_canon(data)
self.assertTrue(result["valid"], f"Fixture failed: {result['errors']}")
def test_mixed_agent_receipt(self):
"""Receipts from different agents can coexist."""
data = deepcopy(json.loads(CANON.read_text()))
data["receipts"] = [
{
"episode_id": "01-pilot",
"form_id": "wizard",
"action": "mutation",
"power_delta": "Lifecycle state",
"scar_delta": "Provenance enforcement",
"cost_delta": "Signed receipts required",
"provenance": "pull/18@562d0cf",
"signed_by": "agent:vincent",
},
{
"episode_id": "01-pilot",
"form_id": "machine",
"action": "graft",
"power_delta": "Execution path proof",
"scar_delta": "Red tallies",
"cost_delta": "Remembers failures",
"provenance": "pull/12@47ca557",
"signed_by": "agent:timmy",
},
]
result = validate_canon(data)
self.assertTrue(result["valid"], f"Mixed fixture failed: {result['errors']}")
def test_human_receipt(self):
"""Human-signed receipts are valid (for canon decisions)."""
data = deepcopy(json.loads(CANON.read_text()))
data["receipts"] = [
{
"episode_id": "01-pilot",
"form_id": "wizard",
"action": "mutation",
"power_delta": "x",
"scar_delta": "x",
"cost_delta": "x",
"provenance": "#7",
"signed_by": "human:grepples",
}
]
result = validate_canon(data)
self.assertTrue(result["valid"], f"Human fixture failed: {result['errors']}")
if __name__ == "__main__":
unittest.main()