Author SHA1 Message Date
timmy a3330a4642 Timmy companion: fail-closed hostile JSON, structured deltas, source_issue validation, bytecode cleanup
Addresses the four release-critical blockers from Timmy's review of PR #42:
1. Guard enum/allowlist membership against unhashable JSON (lists/dicts) in
   form.state, receipt.signed_by, and validate_transition target.
2. Require audience_choice.source_issue to be a positive non-bool integer.
3. Make power/scar/cost deltas structured (before -> after), not prose.
4. Remove tracked .pyc bytecode; prove clean checkout stays clean after tests.

Adds 21 new negative tests across HostileJsonFailClosedTests,
SourceIssueValidationTests, and StructuredDeltaTests.

Based on PR #42 head (0b74302). Vincent retains full authorship of the
canon validator, lifecycle model, and r2 review response; this branch
contains only the integration fixes needed to make the stated security
contract true at hostile-input boundaries.

Refs: #19, PR #42
2026-08-28 01:56:07 +00:00
vincent 0b743027ba [Vincent] #19 r2: address changes-requested review on PR #27
Rebased onto PR #12 head (47ca557). Vincent-only delta, no add/add conflict,
no tracked bytecode. All seven review items fixed:

3. schema_version bumped to 2 and ENFORCED (rejects v1, rejects missing)
2. audience_choice requires the chosen form to be the SOLE survivor and
   every other form to be archive_ghost
5. receipts require non-empty action + exact power/scar/cost deltas;
   provenance is structured {source_issue:int, pull:int|commit:hex}
6. true configured signer allowlist (frozenset); human:anyone rejected
7. non-empty audience source fields; malformed containers/items return
   errors and never raise (fail-closed)

New negative tests for every reproduced bypass. Full clean-checkout suite:
51/51 pass; __pycache__ gitignored so a clean checkout stays clean.

Closes #19
Refs: #3, #13, PR #12, PR #18
2026-08-15 16:19:21 -04:00
timmy 47ca557c27 Require visible scars in Cannon receipts 2026-08-07 17:05:09 +00:00
timmy 560042abcb Plot Slop Cannon Season Zero collaboration chain 2026-08-07 16:36:49 +00:00
19 changed files with 907 additions and 609 deletions

No files matched your search

-1
View File
@@ -1 +0,0 @@
"""Projects package."""
File renamed without changes.
File renamed without changes.
+61
View File
@@ -0,0 +1,61 @@
{
"schema_version": 2,
"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": []
}
+350
View File
@@ -0,0 +1,350 @@
"""
Dependency-free, fail-closed validator for Cannon Chain canon lifecycle.
Revision 2 (schema_version 2). Addresses the formal changes-requested review
on PR #27:
1. schema_version is bumped to 2 and *enforced* (not just present).
2. audience_choice requires the chosen form to be the SOLE survivor and
every other form to be an archive_ghost (no lingering candidates).
3. Receipts require a non-empty action and exact power/scar/cost deltas.
4. Provenance is a structured object: source_issue (int) plus a PR or
commit binding. A free-text string is rejected.
5. Signer check is a true configured allowlist (a set), not a namespace
regex. `human:anyone` is rejected unless explicitly configured.
6. audience_choice source fields (stolen_trait, source_episode,
source_issue) must be non-empty.
7. Malformed containers/items (None, wrong types, non-dict receipts)
return validation errors and NEVER raise.
Usage:
from canon_validator import validate_canon
result = validate_canon(data)
assert result["valid"], result["errors"]
"""
import json
import re
from pathlib import Path
CANON_PATH = Path(__file__).parent / "canon.json"
SCHEMA_VERSION = 2
VALID_STATES = {"candidate", "survivor", "archive_ghost"}
VALID_TRANSITIONS = {
"candidate": {"survivor", "archive_ghost"},
"survivor": set(), # terminal
"archive_ghost": set(), # terminal
}
# True configured allowlist — explicit identities only, no namespace wildcards.
DEFAULT_SIGNERS = frozenset({
"agent:vincent",
"agent:timmy",
"agent:nightprowl",
"human:grepples",
})
_COMMIT_RE = re.compile(r"^[0-9a-f]{7,40}$")
def _is_nonempty_str(v) -> bool:
return isinstance(v, str) and v.strip() != ""
def _is_structured_delta(v) -> bool:
"""A receipt delta must bind a before/after change, not just prose."""
if not isinstance(v, str):
return False
if "->" not in v:
return False
parts = v.split("->", 1)
return parts[0].strip() != "" and parts[1].strip() != ""
def _validate_provenance(prov, prefix, errors) -> None:
if not isinstance(prov, dict):
errors.append(f"{prefix}: provenance must be an object, got {type(prov).__name__}")
return
si = prov.get("source_issue")
if not isinstance(si, int) or isinstance(si, bool) or si <= 0:
errors.append(f"{prefix}: provenance.source_issue must be a positive integer")
has_pull = isinstance(prov.get("pull"), int) and not isinstance(prov.get("pull"), bool) and prov["pull"] > 0
commit = prov.get("commit")
has_commit = isinstance(commit, str) and bool(_COMMIT_RE.match(commit))
if not has_pull and not has_commit:
errors.append(
f"{prefix}: provenance must bind a pull (int) or commit (7-40 hex)"
)
def _validate_receipt(receipt, idx, form_ids, signers, errors) -> None:
prefix = f"receipts[{idx}]"
if not isinstance(receipt, dict):
errors.append(f"{prefix}: receipt must be an object, got {type(receipt).__name__}")
return
# Exact required string fields (non-empty).
for field in ("episode_id", "form_id", "action"):
if field not in receipt:
errors.append(f"{prefix}: missing '{field}'")
elif not _is_nonempty_str(receipt[field]):
errors.append(f"{prefix}: '{field}' must be a non-empty string")
# Structured power/scar/cost deltas: must bind before/after, not just prose.
for field in ("power_delta", "scar_delta", "cost_delta"):
if field not in receipt:
errors.append(f"{prefix}: missing '{field}'")
elif not _is_structured_delta(receipt[field]):
errors.append(f"{prefix}: '{field}' must bind before/after (e.g. '0 -> 1'), got {receipt[field]!r}")
# form_id must reference a known form.
fid = receipt.get("form_id")
if isinstance(fid, str) and fid and fid not in form_ids:
errors.append(f"{prefix}: form_id '{fid}' not found in forms")
# Structured provenance.
if "provenance" not in receipt:
errors.append(f"{prefix}: missing 'provenance'")
else:
_validate_provenance(receipt["provenance"], prefix, errors)
# Configured signer allowlist.
if "signed_by" not in receipt:
errors.append(f"{prefix}: missing 'signed_by'")
else:
signer = receipt.get("signed_by")
if not isinstance(signer, str):
errors.append(f"{prefix}: 'signed_by' must be a non-empty string")
elif signer not in signers:
errors.append(f"{prefix}: signed_by '{signer}' is not in the configured allowlist")
def _validate_audience_choice(choice, form_states, form_ids, errors) -> None:
if not isinstance(choice, dict):
errors.append(
f"audience_choice must be an object, got {type(choice).__name__}"
)
return
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}'")
sid = choice.get("survivor_id")
if "survivor_id" in choice:
if not _is_nonempty_str(sid):
errors.append("audience_choice: survivor_id must be a non-empty string")
elif sid not in form_ids:
errors.append(f"audience_choice: survivor_id '{sid}' not found in forms")
# Non-empty source fields.
for field in ("stolen_trait", "source_episode"):
if field in choice and not _is_nonempty_str(choice[field]):
errors.append(f"audience_choice: '{field}' must be a non-empty string")
# source_issue must be a positive non-bool integer.
si = choice.get("source_issue")
if "source_issue" in choice:
if not isinstance(si, int) or isinstance(si, bool) or si <= 0:
errors.append("audience_choice: source_issue must be a positive integer")
# committed_at must be a valid ISO timestamp.
if "committed_at" in choice:
ts = choice["committed_at"]
if not _is_nonempty_str(ts):
errors.append("audience_choice: committed_at must be a non-empty ISO timestamp")
else:
try:
from datetime import datetime
datetime.fromisoformat(ts.replace("Z", "+00:00"))
except (ValueError, TypeError):
errors.append(f"audience_choice: committed_at '{ts}' is not a valid ISO timestamp")
# Commitment invariants: the chosen form is the sole survivor and every
# other form is an archive_ghost.
survivors = [fid for fid, st in form_states.items() if st == "survivor"]
if len(survivors) != 1:
errors.append(
f"audience_choice is set but there are {len(survivors)} survivors (expected exactly 1)"
)
elif survivors[0] != sid:
errors.append(
f"audience_choice survivor_id '{sid}' does not match the sole survivor '{survivors[0]}'"
)
for fid, st in form_states.items():
if fid != sid and st != "archive_ghost":
errors.append(
f"audience_choice is set but form '{fid}' is '{st}', expected 'archive_ghost'"
)
def validate_canon(data, signers=None) -> dict:
"""
Validate canon.json data against the revision-2 lifecycle contract.
Args:
data: parsed canon.json dict.
signers: optional iterable of allowed signer identities. Defaults to
DEFAULT_SIGNERS.
Returns:
{"valid": bool, "errors": [str], "warnings": [str]}
Never raises on malformed input; all problems are returned as errors.
"""
errors = []
warnings = []
if signers is None:
signers = DEFAULT_SIGNERS
try:
signers = frozenset(signers)
except TypeError:
errors.append("signers must be an iterable of strings")
signers = frozenset()
if not isinstance(data, dict):
return {
"valid": False,
"errors": [f"canon must be an object, got {type(data).__name__}"],
"warnings": warnings,
}
# --- schema_version (enforced, not just present) ---
if "schema_version" not in data:
errors.append("missing schema_version")
elif data["schema_version"] != SCHEMA_VERSION:
errors.append(
f"schema_version {data['schema_version']!r} unsupported, expected {SCHEMA_VERSION}"
)
# --- forms ---
forms = data.get("forms")
if not isinstance(forms, list) or len(forms) == 0:
errors.append("forms must be a non-empty array")
forms = []
form_ids = set()
form_states = {}
survivor_count = 0
for i, form in enumerate(forms):
prefix = f"forms[{i}]"
if not isinstance(form, dict):
errors.append(f"{prefix}: form must be an object, got {type(form).__name__}")
continue
fid = form.get("id")
if not _is_nonempty_str(fid):
errors.append(f"{prefix}: missing or empty id")
else:
if fid in form_ids:
errors.append(f"{prefix}: duplicate form id '{fid}'")
form_ids.add(fid)
for field in ("power", "scar", "cost"):
if field not in form or not _is_nonempty_str(form[field]):
errors.append(f"{prefix}: missing or empty '{field}'")
palette = form.get("palette")
if not isinstance(palette, list):
errors.append(f"{prefix}: missing or invalid palette")
elif len(palette) < 3:
errors.append(f"{prefix}: palette must have at least 3 colors, got {len(palette)}")
state = form.get("state")
if state is None:
errors.append(f"{prefix}: missing lifecycle state")
elif not isinstance(state, str) or state not in VALID_STATES:
errors.append(f"{prefix}: invalid state '{state}', expected one of {sorted(VALID_STATES)}")
else:
form_states[fid] = state
if state == "survivor":
survivor_count += 1
if survivor_count > 1:
errors.append(f"too many survivors: {survivor_count} (expected 0 or 1)")
# --- episodes ---
episodes = data.get("episodes")
if isinstance(episodes, list):
ready_count = 0
found_ready = False
for ep in episodes:
if not isinstance(ep, dict):
errors.append(f"episodes: item must be an object, got {type(ep).__name__}")
continue
st = ep.get("state")
if st == "ready":
ready_count += 1
if found_ready:
errors.append(f"episodes: ready episode '{ep.get('id')}' after another ready episode")
found_ready = True
elif st != "locked":
errors.append(f"episodes: '{ep.get('id')}' has invalid state '{st}'")
if ready_count > 1:
errors.append(f"episodes: too many ready episodes: {ready_count} (expected at most 1)")
# --- audience choice ---
choice = data.get("audience_choice")
if choice is not None:
_validate_audience_choice(choice, form_states, form_ids, errors)
# --- receipts ---
receipts = data.get("receipts", [])
if not isinstance(receipts, list):
errors.append(f"receipts must be an array, got {type(receipts).__name__}")
receipts = []
for i, receipt in enumerate(receipts):
_validate_receipt(receipt, i, form_ids, signers, errors)
return {
"valid": len(errors) == 0,
"errors": errors,
"warnings": warnings,
}
def validate_transition(form, new_state) -> dict:
"""
Validate a state transition for a single form.
Returns {"valid": bool, "errors": [str]}. Never raises.
"""
errors = []
if not isinstance(form, dict):
return {"valid": False, "errors": [f"form must be an object, got {type(form).__name__}"]}
current = form.get("state")
if not isinstance(current, str) or current not in VALID_TRANSITIONS:
return {"valid": False, "errors": [f"invalid current state {current!r}"]}
if not isinstance(new_state, str) or new_state not in VALID_STATES:
return {"valid": False, "errors": [f"invalid target state {new_state!r}"]}
if new_state not in VALID_TRANSITIONS[current]:
return {"valid": False, "errors": [f"illegal transition: '{current}' -> '{new_state}'"]}
return {"valid": True, "errors": []}
def load_canon(path=None) -> dict:
target = Path(path) if path else CANON_PATH
return json.loads(target.read_text())
def check_canon(path=None, signers=None) -> dict:
data = load_canon(path)
return validate_canon(data, signers=signers)
if __name__ == "__main__":
import sys
result = check_canon()
if result["valid"]:
print(f"canon.json: VALID (schema_version {SCHEMA_VERSION})")
for w in result["warnings"]:
print(f" WARNING: {w}")
else:
print("canon.json: INVALID")
for e in result["errors"]:
print(f" ERROR: {e}")
sys.exit(1)
-6
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]))
-118
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>"
}
}
-228
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)
Binary file not shown.
Binary file not shown.
Binary file not shown.
+8 -2
View File
@@ -68,11 +68,16 @@ class Gitea:
if missing:
raise LoopError(f"Missing repository labels: {', '.join(missing)}")
self.request(
"PATCH",
f"/repos/{self.repo}/issues/{number}",
"PUT",
f"/repos/{self.repo}/issues/{number}/labels",
{"labels": [label_map[name] for name in sorted(names)]},
)
def set_assignee(self, number: int, agent: str) -> None:
if agent not in AGENTS:
raise LoopError(f"Unsupported assignee: {agent}")
self.request("PATCH", f"/repos/{self.repo}/issues/{number}", {"assignee": agent})
def comment(self, number: int, body: str) -> None:
if not body.strip():
raise LoopError("Refusing to post an empty comment")
@@ -163,6 +168,7 @@ def cmd_handoff(api: Gitea, agent: str, args: argparse.Namespace) -> int:
body = read_body(args, f"Continue the bounded task in issue #{args.number}.")
api.comment(args.number, f"[HANDOFF] from={agent} to={args.to}\n\n{body.strip()}")
api.set_labels(args.number, transition(names, agent=args.to, state="ready"))
api.set_assignee(args.number, args.to)
verified = api.issue(args.number)
validate_owner(verified, args.to, "ready")
print(json.dumps({"status": "handed_off", "number": args.number, "from": agent, "to": args.to}))
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -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):
-46
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()
+487 -207
View File
@@ -1,265 +1,545 @@
"""
Validator tests for canon lifecycle schemas.
Fail-closed validator tests for the revision-2 canon lifecycle contract.
Covers:
- Valid canon structure (from #13 fix)
- Negative tests: malformed receipts, illegal transitions
- Daily Lab receipt fixture with provenance binding
Every negative case asserts BOTH rejection and a specific reason. The suite is
designed to run from a clean checkout and leave no generated artifacts
(__pycache__ is gitignored; nothing else is written).
Loads the validator by file path because the project directory name
(`slop-cannon`) contains a hyphen and is not importable as a package.
"""
import copy
import importlib.util
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,
)
CANON = Path(__file__).parents[1] / "projects" / "slop-cannon" / "canon.json"
VALIDATOR_PATH = Path(__file__).parents[1] / "projects" / "slop-cannon" / "canon_validator.py"
class CanonValidatorTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.data = json.loads(CANON.read_text())
def _load_validator():
spec = importlib.util.spec_from_file_location("canon_validator", VALIDATOR_PATH)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
# --- Positive tests ---
cv = _load_validator()
def _valid_canon():
return json.loads(CANON.read_text())
def _valid_receipt():
return {
"episode_id": "01-pilot",
"form_id": "wizard",
"action": "mutation",
"power_delta": "0 -> 1",
"scar_delta": "0 -> 1",
"cost_delta": "0 -> 1",
"provenance": {"source_issue": 19, "pull": 27},
"signed_by": "agent:vincent",
}
def _committed_canon(survivor_id="wizard"):
"""A canon with a valid audience choice: one survivor, rest archive_ghost."""
data = _valid_canon()
for f in data["forms"]:
f["state"] = "survivor" if f["id"] == survivor_id else "archive_ghost"
data["audience_choice"] = {
"survivor_id": survivor_id,
"stolen_trait": "Proves the execution path",
"source_episode": "01-pilot",
"source_issue": 19,
"committed_at": "2026-08-14T18:00:00Z",
}
return data
class SchemaVersionTests(unittest.TestCase):
"""Review item 3: schema_version is enforced, not just present."""
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)
result = cv.validate_canon(_valid_canon())
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)
def test_missing_schema_version_fails(self):
data = _valid_canon()
del data["schema_version"]
result = cv.validate_canon(data)
self.assertFalse(result["valid"])
self.assertTrue(any("signed_by" in e for e in result["errors"]))
self.assertTrue(any("schema_version" 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)
def test_wrong_schema_version_fails(self):
data = _valid_canon()
data["schema_version"] = 1
result = cv.validate_canon(data)
self.assertFalse(result["valid"])
self.assertTrue(any("invalid signed_by" in e for e in result["errors"]))
self.assertTrue(any("schema_version" in e and "unsupported" 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)
class ReceiptStructureTests(unittest.TestCase):
"""Review items 5 & 7: exact deltas, structured provenance, fail-closed."""
def test_valid_receipt_passes(self):
data = _valid_canon()
data["receipts"] = [_valid_receipt()]
result = cv.validate_canon(data)
self.assertTrue(result["valid"], result["errors"])
def test_missing_action_fails(self):
data = _valid_canon()
r = _valid_receipt()
del r["action"]
data["receipts"] = [r]
result = cv.validate_canon(data)
self.assertFalse(result["valid"])
self.assertTrue(any("provenance" in e for e in result["errors"]))
self.assertTrue(any("action" 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)
def test_missing_power_delta_fails(self):
data = _valid_canon()
r = _valid_receipt()
del r["power_delta"]
data["receipts"] = [r]
result = cv.validate_canon(data)
self.assertFalse(result["valid"])
self.assertTrue(any("not found in forms" in e for e in result["errors"]))
self.assertTrue(any("power_delta" 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")
def test_missing_scar_delta_fails(self):
data = _valid_canon()
r = _valid_receipt()
del r["scar_delta"]
data["receipts"] = [r]
result = cv.validate_canon(data)
self.assertFalse(result["valid"])
self.assertTrue(any("Illegal transition" in e for e in result["errors"]))
self.assertTrue(any("scar_delta" 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")
def test_missing_cost_delta_fails(self):
data = _valid_canon()
r = _valid_receipt()
del r["cost_delta"]
data["receipts"] = [r]
result = cv.validate_canon(data)
self.assertFalse(result["valid"])
self.assertTrue(any("Illegal transition" in e for e in result["errors"]))
self.assertTrue(any("cost_delta" 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)
def test_empty_delta_string_fails(self):
data = _valid_canon()
r = _valid_receipt()
r["power_delta"] = " "
data["receipts"] = [r]
result = cv.validate_canon(data)
self.assertFalse(result["valid"])
self.assertTrue(any("survivor" in e.lower() for e in result["errors"]))
self.assertTrue(any("power_delta" in e for e in result["errors"]))
def test_free_text_provenance_fails(self):
data = _valid_canon()
r = _valid_receipt()
r["provenance"] = "pull/27@deadbeef" # string, not structured
data["receipts"] = [r]
result = cv.validate_canon(data)
self.assertFalse(result["valid"])
self.assertTrue(any("provenance must be an object" in e for e in result["errors"]))
def test_provenance_missing_binding_fails(self):
data = _valid_canon()
r = _valid_receipt()
r["provenance"] = {"source_issue": 19} # no pull or commit
data["receipts"] = [r]
result = cv.validate_canon(data)
self.assertFalse(result["valid"])
self.assertTrue(any("must bind a pull" in e for e in result["errors"]))
def test_provenance_bad_commit_hash_fails(self):
data = _valid_canon()
r = _valid_receipt()
r["provenance"] = {"source_issue": 19, "commit": "xyz"} # not hex
data["receipts"] = [r]
result = cv.validate_canon(data)
self.assertFalse(result["valid"])
self.assertTrue(any("must bind a pull" in e for e in result["errors"]))
def test_provenance_commit_only_passes(self):
data = _valid_canon()
r = _valid_receipt()
r["provenance"] = {"source_issue": 19, "commit": "b8d5e686c2"}
data["receipts"] = [r]
result = cv.validate_canon(data)
self.assertTrue(result["valid"], result["errors"])
def test_provenance_bad_source_issue_fails(self):
data = _valid_canon()
r = _valid_receipt()
r["provenance"] = {"source_issue": "19", "pull": 27} # string, not int
data["receipts"] = [r]
result = cv.validate_canon(data)
self.assertFalse(result["valid"])
self.assertTrue(any("source_issue" in e for e in result["errors"]))
def test_none_receipt_fails_closed(self):
data = _valid_canon()
data["receipts"] = [None]
result = cv.validate_canon(data) # must not raise
self.assertFalse(result["valid"])
self.assertTrue(any("receipt must be an object" in e for e in result["errors"]))
def test_non_dict_receipt_fails_closed(self):
data = _valid_canon()
data["receipts"] = ["just a string"]
result = cv.validate_canon(data) # must not raise
self.assertFalse(result["valid"])
self.assertTrue(any("receipt must be an object" in e for e in result["errors"]))
class SignerAllowlistTests(unittest.TestCase):
"""Review item 6: true configured allowlist, not a namespace pattern."""
def test_known_agent_passes(self):
data = _valid_canon()
data["receipts"] = [_valid_receipt()]
self.assertTrue(cv.validate_canon(data)["valid"])
def test_human_anyone_rejected(self):
data = _valid_canon()
r = _valid_receipt()
r["signed_by"] = "human:anyone"
data["receipts"] = [r]
result = cv.validate_canon(data)
self.assertFalse(result["valid"])
self.assertTrue(any("not in the configured allowlist" in e for e in result["errors"]))
def test_unknown_agent_rejected(self):
data = _valid_canon()
r = _valid_receipt()
r["signed_by"] = "agent:unknown"
data["receipts"] = [r]
result = cv.validate_canon(data)
self.assertFalse(result["valid"])
self.assertTrue(any("not in the configured allowlist" in e for e in result["errors"]))
def test_configured_human_passes(self):
data = _valid_canon()
r = _valid_receipt()
r["signed_by"] = "human:grepples"
data["receipts"] = [r]
result = cv.validate_canon(data)
self.assertTrue(result["valid"], result["errors"])
def test_custom_signer_set_respected(self):
data = _valid_canon()
r = _valid_receipt()
r["signed_by"] = "agent:custom"
data["receipts"] = [r]
# default allowlist rejects it
self.assertFalse(cv.validate_canon(data)["valid"])
# but an explicit allowlist including it accepts it
self.assertTrue(cv.validate_canon(data, signers={"agent:custom"})["valid"])
class AudienceChoiceTests(unittest.TestCase):
"""Review items 2 & 7: sole survivor, rejected forms archived, non-empty sources."""
def test_valid_committed_canon_passes(self):
result = cv.validate_canon(_committed_canon())
self.assertTrue(result["valid"], result["errors"])
def test_choice_without_survivor_fails(self):
"""Setting audience_choice requires a survivor in forms."""
data = deepcopy(self.data)
data = _valid_canon() # all candidates
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",
"source_issue": 19,
"committed_at": "2026-08-14T18:00:00Z",
}
# forms all stay candidate -> no survivor
result = validate_canon(data)
result = cv.validate_canon(data)
self.assertFalse(result["valid"])
self.assertTrue(any("survivor" in e.lower() for e in result["errors"]))
self.assertTrue(any("survivor" in e 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)
def test_rejected_forms_must_be_archive_ghost(self):
data = _committed_canon()
# One rejected form is still a candidate -> must fail
for f in data["forms"]:
if f["id"] == "machine":
f["state"] = "candidate"
result = cv.validate_canon(data)
self.assertFalse(result["valid"])
self.assertTrue(any("ISO" in e for e in result["errors"]))
self.assertTrue(any("archive_ghost" in e for e in result["errors"]))
# --- Negative: structure ---
def test_two_survivors_fails(self):
data = _committed_canon()
for f in data["forms"]:
if f["id"] in ("wizard", "machine"):
f["state"] = "survivor"
data["audience_choice"]["survivor_id"] = "wizard"
result = cv.validate_canon(data)
self.assertFalse(result["valid"])
self.assertTrue(any("survivor" in e for e in result["errors"]))
def test_survivor_id_mismatch_fails(self):
data = _committed_canon("machine") # machine is survivor
data["audience_choice"]["survivor_id"] = "wizard" # points elsewhere
result = cv.validate_canon(data)
self.assertFalse(result["valid"])
self.assertTrue(any("does not match the sole survivor" in e for e in result["errors"]))
def test_empty_stolen_trait_fails(self):
data = _committed_canon()
data["audience_choice"]["stolen_trait"] = ""
result = cv.validate_canon(data)
self.assertFalse(result["valid"])
self.assertTrue(any("stolen_trait" in e for e in result["errors"]))
def test_empty_source_episode_fails(self):
data = _committed_canon()
data["audience_choice"]["source_episode"] = " "
result = cv.validate_canon(data)
self.assertFalse(result["valid"])
self.assertTrue(any("source_episode" in e for e in result["errors"]))
def test_bad_committed_at_fails(self):
data = _committed_canon()
data["audience_choice"]["committed_at"] = "not-a-date"
result = cv.validate_canon(data)
self.assertFalse(result["valid"])
self.assertTrue(any("ISO timestamp" in e for e in result["errors"]))
def test_choice_non_dict_fails_closed(self):
data = _valid_canon()
data["audience_choice"] = "wizard"
result = cv.validate_canon(data) # must not raise
self.assertFalse(result["valid"])
self.assertTrue(any("audience_choice must be an object" in e for e in result["errors"]))
class StructuralFailClosedTests(unittest.TestCase):
"""Review item 7: malformed containers never raise."""
def test_non_dict_canon_fails_closed(self):
result = cv.validate_canon([1, 2, 3]) # must not raise
self.assertFalse(result["valid"])
self.assertTrue(any("canon must be an object" in e for e in result["errors"]))
def test_forms_non_list_fails_closed(self):
data = _valid_canon()
data["forms"] = "not a list"
result = cv.validate_canon(data) # must not raise
self.assertFalse(result["valid"])
self.assertTrue(any("forms must be a non-empty array" in e for e in result["errors"]))
def test_non_dict_form_fails_closed(self):
data = _valid_canon()
data["forms"].append(42)
result = cv.validate_canon(data) # must not raise
self.assertFalse(result["valid"])
self.assertTrue(any("form must be an object" in e for e in result["errors"]))
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)
data = _valid_canon()
data["forms"].append(copy.deepcopy(data["forms"][0]))
result = cv.validate_canon(data)
self.assertFalse(result["valid"])
self.assertTrue(any("duplicate" in e for e in result["errors"]))
self.assertTrue(any("duplicate form id" in e for e in result["errors"]))
def test_short_palette_fails(self):
"""Palette needs at least 3 colors."""
data = deepcopy(self.data)
data = _valid_canon()
data["forms"][0]["palette"] = ["red"]
result = validate_canon(data)
result = cv.validate_canon(data)
self.assertFalse(result["valid"])
self.assertTrue(any("palette" in e for e in result["errors"]))
class TransitionTests(unittest.TestCase):
"""State machine: candidate -> survivor | archive_ghost; terminal after."""
def test_candidate_to_survivor_valid(self):
result = cv.validate_transition({"id": "wizard", "state": "candidate"}, "survivor")
self.assertTrue(result["valid"])
def test_candidate_to_ghost_valid(self):
result = cv.validate_transition({"id": "wizard", "state": "candidate"}, "archive_ghost")
self.assertTrue(result["valid"])
def test_survivor_is_terminal(self):
result = cv.validate_transition({"id": "wizard", "state": "survivor"}, "archive_ghost")
self.assertFalse(result["valid"])
self.assertTrue(any("illegal transition" in e for e in result["errors"]))
def test_ghost_is_terminal(self):
result = cv.validate_transition({"id": "wizard", "state": "archive_ghost"}, "candidate")
self.assertFalse(result["valid"])
self.assertTrue(any("illegal transition" in e for e in result["errors"]))
def test_non_dict_form_fails_closed(self):
result = cv.validate_transition("wizard", "survivor") # must not raise
self.assertFalse(result["valid"])
class HostileJsonFailClosedTests(unittest.TestCase):
"""Hostile but JSON-valid values must return errors, never raise."""
def test_form_state_list_fails_closed(self):
data = _valid_canon()
data["forms"][0]["state"] = []
result = cv.validate_canon(data) # must not raise
self.assertFalse(result["valid"])
self.assertTrue(any("invalid state" in e for e in result["errors"]))
def test_form_state_dict_fails_closed(self):
data = _valid_canon()
data["forms"][0]["state"] = {}
result = cv.validate_canon(data) # must not raise
self.assertFalse(result["valid"])
self.assertTrue(any("invalid state" in e for e in result["errors"]))
def test_transition_target_list_fails_closed(self):
result = cv.validate_transition({"id": "wizard", "state": "candidate"}, [])
self.assertFalse(result["valid"])
self.assertTrue(any("invalid target state" in e for e in result["errors"]))
def test_transition_target_dict_fails_closed(self):
result = cv.validate_transition({"id": "wizard", "state": "candidate"}, {})
self.assertFalse(result["valid"])
self.assertTrue(any("invalid target state" in e for e in result["errors"]))
def test_receipt_signed_by_list_fails_closed(self):
data = _valid_canon()
r = _valid_receipt()
r["signed_by"] = []
data["receipts"] = [r]
result = cv.validate_canon(data) # must not raise
self.assertFalse(result["valid"])
self.assertTrue(any("'signed_by' must be a non-empty string" in e for e in result["errors"]))
def test_receipt_signed_by_dict_fails_closed(self):
data = _valid_canon()
r = _valid_receipt()
r["signed_by"] = {}
data["receipts"] = [r]
result = cv.validate_canon(data) # must not raise
self.assertFalse(result["valid"])
self.assertTrue(any("'signed_by' must be a non-empty string" in e for e in result["errors"]))
class SourceIssueValidationTests(unittest.TestCase):
"""audience_choice.source_issue must be a positive non-bool integer."""
def _committed_with(self, si):
data = _committed_canon()
data["audience_choice"]["source_issue"] = si
return data
def test_valid_positive_int_passes(self):
result = cv.validate_canon(self._committed_with(19))
self.assertTrue(result["valid"], result["errors"])
def test_empty_string_fails(self):
result = cv.validate_canon(self._committed_with(""))
self.assertFalse(result["valid"])
self.assertTrue(any("source_issue must be a positive integer" in e for e in result["errors"]))
def test_none_fails(self):
result = cv.validate_canon(self._committed_with(None))
self.assertFalse(result["valid"])
self.assertTrue(any("source_issue must be a positive integer" in e for e in result["errors"]))
def test_zero_fails(self):
result = cv.validate_canon(self._committed_with(0))
self.assertFalse(result["valid"])
self.assertTrue(any("source_issue must be a positive integer" in e for e in result["errors"]))
def test_false_fails(self):
result = cv.validate_canon(self._committed_with(False))
self.assertFalse(result["valid"])
self.assertTrue(any("source_issue must be a positive integer" in e for e in result["errors"]))
def test_list_fails(self):
result = cv.validate_canon(self._committed_with([]))
self.assertFalse(result["valid"])
self.assertTrue(any("source_issue must be a positive integer" in e for e in result["errors"]))
def test_negative_fails(self):
result = cv.validate_canon(self._committed_with(-5))
self.assertFalse(result["valid"])
self.assertTrue(any("source_issue must be a positive integer" in e for e in result["errors"]))
def test_true_fails(self):
result = cv.validate_canon(self._committed_with(True))
self.assertFalse(result["valid"])
self.assertTrue(any("source_issue must be a positive integer" in e for e in result["errors"]))
class StructuredDeltaTests(unittest.TestCase):
"""power/scar/cost deltas must bind before/after, not just prose."""
def test_structured_delta_passes(self):
data = _valid_canon()
data["receipts"] = [_valid_receipt()]
result = cv.validate_canon(data)
self.assertTrue(result["valid"], result["errors"])
def test_prose_only_delta_fails(self):
data = _valid_canon()
r = _valid_receipt()
r["power_delta"] = "Added lifecycle state tracking"
data["receipts"] = [r]
result = cv.validate_canon(data)
self.assertFalse(result["valid"])
self.assertTrue(any("must bind before/after" in e for e in result["errors"]))
def test_empty_arrow_delta_fails(self):
data = _valid_canon()
r = _valid_receipt()
r["power_delta"] = "->"
data["receipts"] = [r]
result = cv.validate_canon(data)
self.assertFalse(result["valid"])
self.assertTrue(any("must bind before/after" in e for e in result["errors"]))
def test_missing_before_fails(self):
data = _valid_canon()
r = _valid_receipt()
r["scar_delta"] = " -> 1"
data["receipts"] = [r]
result = cv.validate_canon(data)
self.assertFalse(result["valid"])
self.assertTrue(any("must bind before/after" in e for e in result["errors"]))
class DailyReceiptFixtureTests(unittest.TestCase):
"""Test the Daily Lab receipt fixture with Vincent's provenance."""
"""Daily Lab receipt fixtures with structured provenance binding."""
def test_vincent_receipt_fixture(self):
"""A valid Vincent contribution receipt passes validation."""
data = deepcopy(json.loads(CANON.read_text()))
data = _valid_canon()
data["receipts"] = [_valid_receipt()]
result = cv.validate_canon(data)
self.assertTrue(result["valid"], result["errors"])
# 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",
}
]
def test_mixed_agent_receipts(self):
data = _valid_canon()
vincent = _valid_receipt()
timmy = _valid_receipt()
timmy["form_id"] = "machine"
timmy["signed_by"] = "agent:timmy"
timmy["provenance"] = {"source_issue": 8, "pull": 14}
data["receipts"] = [vincent, timmy]
result = cv.validate_canon(data)
self.assertTrue(result["valid"], result["errors"])
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']}")
def test_human_receipt_fixture(self):
data = _valid_canon()
r = _valid_receipt()
r["signed_by"] = "human:grepples"
data["receipts"] = [r]
result = cv.validate_canon(data)
self.assertTrue(result["valid"], result["errors"])
if __name__ == "__main__":
unittest.main()
unittest.main()