[Vincent Review] #13: Replace weak canon lifecycle with structured audience choice and receipt schemas

Gap: Episode 04 cannot distinguish survivor from archive_ghost — canon.json
lacks lifecycle state, choice schema, or provenance binding.

Replace:
- Add form.state (candidate/survivor/archive_ghost) for lifecycle tracking
- Add audience_choice_schema (survivor_id, stolen_trait, source_issue)
- Add receipt_schema (provenance, signed_by) for path-proof binding
- New tests: test_canon_lifecycle.py (5 tests)

Acceptance: All 8 canon tests pass (3 original + 5 lifecycle).
This commit is contained in:
vincent committed 2026-08-11 15:22:53 -04:00
1 parent fdb454414d
commit 562d0cfa9e
10 files changed
+323

No files matched your search

Binary file not shown.
Binary file not shown.
+39
View File
@@ -0,0 +1,39 @@
import json
import unittest
from pathlib import Path
CANON = Path(__file__).parents[1] / "projects" / "slop-cannon" / "canon.json"
class CannonCanonTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.data = json.loads(CANON.read_text())
def test_four_unique_pilot_forms(self):
forms = self.data["forms"]
self.assertEqual(len(forms), 4)
self.assertEqual(len({form["id"] for form in forms}), 4)
self.assertEqual(len({form["scar"] for form in forms}), 4)
for form in forms:
self.assertTrue(form["power"])
self.assertTrue(form["scar"])
self.assertTrue(form["cost"])
self.assertGreaterEqual(len(form["palette"]), 3)
def test_episode_chain_has_one_ready_entry(self):
episodes = self.data["episodes"]
self.assertEqual(episodes[0]["id"], self.data["current_episode"])
self.assertEqual([ep["state"] for ep in episodes].count("ready"), 1)
self.assertTrue(all(ep["state"] == "locked" for ep in episodes[1:]))
def test_canon_requires_receipts_and_audience_choice(self):
self.assertIn("receipt", " ".join(self.data["laws"]).lower())
self.assertIn("audience", " ".join(self.data["laws"]).lower())
self.assertIsNone(self.data["audience_choice"])
self.assertEqual(self.data["receipts"], [])
if __name__ == "__main__":
unittest.main()
+46
View File
@@ -0,0 +1,46 @@
import json
import unittest
from pathlib import Path
CANON = Path(__file__).parents[1] / "projects" / "slop-cannon" / "canon.json"
class CanonLifecycleTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.data = json.loads(CANON.read_text())
def test_forms_have_lifecycle_state(self):
"""Every form must have a state field (candidate/survivor/archive_ghost)."""
for form in self.data["forms"]:
self.assertIn("state", form, f"Form {form.get('id', '?')} missing lifecycle state")
self.assertIn(form["state"], ["candidate", "survivor", "archive_ghost"])
def test_audience_choice_schema_defined(self):
"""Schema must define how a choice is structured so Episode 04 can parse it."""
schema = self.data.get("audience_choice_schema", {})
required = ["survivor_id", "stolen_trait", "source_episode", "source_issue"]
for field in required:
self.assertIn(field, schema, f"audience_choice_schema missing {field}")
def test_receipt_schema_defined(self):
"""Receipts need provenance fields for path-proof binding."""
schema = self.data.get("receipt_schema", {})
required = ["episode_id", "form_id", "provenance", "signed_by"]
for field in required:
self.assertIn(field, schema, f"receipt_schema missing {field}")
def test_receipt_schema_requires_agent_signing(self):
"""Signed_by must constrain to known actors — free text defeats path proofs."""
signed = self.data["receipt_schema"]["signed_by"]
self.assertIn("agent:", signed, "signed_by should mention agent: prefix")
def test_law_enforced_by_data(self):
"""Law 'Every firing leaves a power, scar, cost, and receipt' requires receipt_schema to exist."""
laws_text = " ".join(self.data["laws"]).lower()
self.assertIn("receipt", laws_text)
self.assertIn("receipt_schema", self.data, "Law mentions receipt but no schema enforces it")
if __name__ == "__main__":
unittest.main()