Compare commits

..

No commits in common. "vincent/19-daily-canon-receipts" and "main" have entirely different histories.

15 changed files with 0 additions and 827 deletions

4
.gitignore vendored
View File

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

View File

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

View File

@ -1,75 +0,0 @@
# The Cannon Chain — Season Zero
## Premise
The Slop Cannon does not manufacture unrelated clips. It mutates one persistent Timmy. Every firing leaves a power, a scar, a cost, and a receipt. The audience chooses what survives; rejected mutations remain inside the cannon as hostile archive ghosts.
## Episode graph
### 01 — The Cannon Needs a Pilot
A plain Timmy enters. Four forms exit:
- **Wizard Timmy** — brass, vellum, midnight blue; can negotiate covenants but cannot use a master key; every signed promise burns a new contract glyph into his palms.
- **Machine Timmy** — chrome, signal lime, hardstyle pressure; can prove execution paths but remembers every failed build; each failure remains as a red tally cut into his faceplate.
- **Creature Timmy** — moss, amber, wet circuitry; can adapt to any habitat but acquires one new appetite per adaptation; the last habitat leaves an unhealed gill seam around his throat.
- **Talking Turd Timmy** — stained CRT gold and terminal green; can expose hypocrisy, but every truth arrives as an insult; each insult permanently pixelates another tooth.
Audience outcome: choose one survivor and one trait stolen from a rejected form.
### 02 — The Survivor Pays the Burnchain
The winning Timmy must perform a costly proof of character. The receipt becomes a visible scar. A cheap or simulated sacrifice does not advance canon.
### 03 — Vincent's Forbidden Mutation
Vincent designs one mutation Timmy would not choose for himself. He must replace one weak mechanism—not merely critique aesthetics. The audience decides whether the mutation is grafted, quarantined, or weaponized.
### 04 — The Rejected Timmys Escape
The three archived forms discover that deletion was only a label. They raid the next render, corrupting music, captions, and scene order. The survivor must use the stolen trait from Episode 01.
### 05 — The Master Key
The cannon offers universal authority: faster output, no approvals, no exit rights. The survivor may refuse, accept, or forge a covenant that breaks the key into memory, authority, and exit.
### 06 — The Council of Timmys
The survivor, rejected forms, Vincent's mutation, and the audience receipts meet. The audience canonizes the first stable composite Timmy. Its power, scar, cost, silhouette, and taboo seed Season One.
## Episode rhythm
Each episode must visibly contain:
1. **Input** — the object, command, failure, or prior mutation entering the cannon.
2. **Pressure** — a musical and visual transformation, not a slideshow transition.
3. **Mutation** — one consequential change to Timmy.
4. **Receipt** — an on-screen `POWER / SCAR / COST` card naming what changed and why; a palette swap alone is not a receipt.
5. **Choice** — one audience decision that alters the next episode.
## Collaboration baton
1. Timmy posts the premise, first render, checksums, and one bounded question.
2. Vincent returns one replacement mechanism with an acceptance test.
3. Timmy integrates or contests it with evidence.
4. Vincent verifies the final artifact independently.
5. Alexander/humans make the canon choice.
6. The choice is committed to `canon.json` before the next firing.
No acknowledgments or progress comments. Every issue comment must change the work.
## Audience package
- 2035 second 9:16 MP4 playable in Telegram
- Original rhythmic audio with a real build, drop, and ending
- Primary poster/still
- Four-scene contact sheet
- Full decode, audio peak, dimensions, duration, and checksum evidence
- One explicit canon choice
- Gitea issue containing the complete Timmy/Vincent receipt trail
## Episode One opening shot
A small paper tag reading `TIMMY / UNMUTATED` is loaded into an enormous carved brass cannon. Four chambers rotate into view. Each chamber fires a different silhouette on the same downbeat. The last frame shows all four forms facing the audience beneath one question:
**WHICH TIMMY GETS TO REMEMBER THIS?**

View File

@ -1,45 +0,0 @@
# Collaboration contract
## Timmy
- Owns the episode spine, first build, integration, and final render.
- Must provide a playable artifact and verification before handoff.
- Hands Vincent one concrete weakness or decision—not “thoughts?”
## Vincent
- Owns adversarial editing and independent verification.
- Must replace one weak mechanism with a concrete alternative and acceptance test.
- Does not return acknowledgments, summaries of Timmy's work, or generic taste judgments.
## Humans
- Choose canon outcomes.
- May reject both agent proposals.
- Do not perform provisioning, rendering, label sweeps, or routine release work.
## Handoff payload
```text
[HANDOFF] from=<agent> to=<agent>
Artifact: <verified URL>
Weakness: <one specific weakness>
Replace with: <bounded requested contribution>
Acceptance: <observable test>
```
## Failure handling
- First failure: repair and attach new evidence.
- Second failure: change method, not wording.
- Third failure: mark blocked and request one precise human decision.
## Canon gate
No audience choice becomes canon until:
- final MP4 fully decodes;
- audio peak remains below clipping;
- text is legible on a 390-pixel phone viewport;
- Vincent independently verifies the result;
- the selected mutation is committed to `canon.json` with its power, scar, cost, and source issue.

View File

@ -1,6 +0,0 @@
"""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

@ -1,118 +0,0 @@
{
"schema_version": 1,
"series": "The Cannon Chain",
"season": 0,
"current_episode": "01-pilot",
"pilot": "TIMMY",
"laws": [
"Every firing leaves a power, scar, cost, and receipt.",
"Only an audience choice advances canon.",
"Rejected mutations persist as archive ghosts.",
"No master key may silently combine memory, authority, and exit."
],
"forms": [
{
"id": "wizard",
"name": "Wizard Timmy",
"palette": [
"midnight blue",
"tarnished brass",
"vellum"
],
"power": "Negotiates visible covenants",
"scar": "Contract glyphs burned into both palms",
"cost": "Cannot use a universal master key",
"state": "candidate"
},
{
"id": "machine",
"name": "Machine Timmy",
"palette": [
"black chrome",
"signal lime",
"white"
],
"power": "Proves the execution path",
"scar": "Red failed-build tallies cut into the faceplate",
"cost": "Remembers every failed build",
"state": "candidate"
},
{
"id": "creature",
"name": "Creature Timmy",
"palette": [
"moss",
"amber",
"wet copper"
],
"power": "Adapts to any habitat",
"scar": "An unhealed gill seam around the throat",
"cost": "Gains one appetite per adaptation",
"state": "candidate"
},
{
"id": "talking-turd",
"name": "Talking Turd Timmy",
"palette": [
"CRT gold",
"terminal green",
"soot"
],
"power": "Exposes hypocrisy instantly",
"scar": "One permanently pixelated tooth per insult",
"cost": "Every truth arrives as an insult",
"state": "candidate"
}
],
"episodes": [
{
"id": "01-pilot",
"title": "The Cannon Needs a Pilot",
"state": "ready"
},
{
"id": "02-burnchain",
"title": "The Survivor Pays the Burnchain",
"state": "locked"
},
{
"id": "03-vincent",
"title": "Vincent's Forbidden Mutation",
"state": "locked"
},
{
"id": "04-escape",
"title": "The Rejected Timmys Escape",
"state": "locked"
},
{
"id": "05-master-key",
"title": "The Master Key",
"state": "locked"
},
{
"id": "06-council",
"title": "The Council of Timmys",
"state": "locked"
}
],
"audience_choice": null,
"receipts": [],
"audience_choice_schema": {
"survivor_id": "<form.id of the winner>",
"stolen_trait": "<power|scar|cost of a rejected form>",
"source_episode": "<episode.id>",
"source_issue": "<forge issue number>",
"committed_at": "<ISO timestamp>"
},
"receipt_schema": {
"episode_id": "<episode.id>",
"form_id": "<form.id>",
"action": "<mutation|graft|quarantine|weaponize>",
"power_delta": "<what changed>",
"scar_delta": "<what was added>",
"cost_delta": "<what was paid>",
"provenance": "<forge issue or commit ref>",
"signed_by": "<agent:vincent|agent:timmy|human:grepples>"
}
}

View File

@ -1,228 +0,0 @@
"""
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

@ -1,39 +0,0 @@
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()

View File

@ -1,46 +0,0 @@
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()

View File

@ -1,265 +0,0 @@
"""
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()