Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b743027ba | ||
|
|
47ca557c27 | ||
|
|
560042abcb |
No files matched your search
@@ -0,0 +1,4 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
.pytest_cache/
|
||||
@@ -1,58 +0,0 @@
|
||||
# Season Zero — work-derived X recut
|
||||
|
||||
A deterministic 30-second vertical teaser replacing the rejected abstract visualizer with a legible evidence chain:
|
||||
|
||||
1. the live Stackchain Lab issue queue rendered as a work board, plus Season Zero epic #7;
|
||||
2. Vincent's issue #1 contribution receipt and independent `8 TESTS PASS` verification;
|
||||
3. the verified 2026-08-07 Daily Drop artwork and Slop Cannon scene 04 as transformation inputs;
|
||||
4. the four Episode 01 mutation cards extracted from the verified pilot render;
|
||||
5. the audience gate: **WHICH TIMMY GETS TO REMEMBER THIS?**
|
||||
|
||||
The persistent pipeline rail, moving scan, board drift, source pan/zoom, transfer beam, card reveals, and orbiting final forms make one continuous transformation rather than a static screenshot slideshow.
|
||||
|
||||
## Artifacts
|
||||
|
||||
- [`stackchain-season-zero-work-recut.mp4`](stackchain-season-zero-work-recut.mp4) — delivery master
|
||||
- [`contact-sheet.jpg`](contact-sheet.jpg) — six-frame review strip
|
||||
- [`stills/`](stills/) — five representative full-size review frames
|
||||
- [`build_work_recut.py`](build_work_recut.py) — deterministic renderer and original synthesized soundtrack
|
||||
- [`source/`](source/) — checksummed work-derived image inputs
|
||||
|
||||
## Revised X copy — 216 characters
|
||||
|
||||
> Real work enters the Slop Cannon: a live Timmy/Vincent queue, contribution receipts, 8/8 verification, Daily Drop art, and four mutations.
|
||||
>
|
||||
> Wizard. Machine. Creature. Talking Turd.
|
||||
>
|
||||
> Which Timmy gets to remember this?
|
||||
|
||||
**Do not publish without approval in human gate #15.**
|
||||
|
||||
## Verification
|
||||
|
||||
```text
|
||||
container: MP4
|
||||
video: H.264, 720x1280 (9:16), yuv420p, 30 fps
|
||||
audio: AAC, 48 kHz, mono
|
||||
duration: 30.000000 seconds
|
||||
size: 5,325,465 bytes
|
||||
full decode: pass
|
||||
encoded audio: -20.1 dB mean, -2.2 dB max
|
||||
sha256: 2436d193e5d7ee0660318f585d5b84356cbf623a034d63355ed9232258c1f08d
|
||||
```
|
||||
|
||||
Re-run:
|
||||
|
||||
```bash
|
||||
python3 media/x-teaser/build_work_recut.py
|
||||
ffprobe -v error -show_entries format=duration,size:stream=codec_name,codec_type,width,height,pix_fmt,r_frame_rate,sample_rate,channels -of json media/x-teaser/stackchain-season-zero-work-recut.mp4
|
||||
ffmpeg -v error -i media/x-teaser/stackchain-season-zero-work-recut.mp4 -f null -
|
||||
sha256sum media/x-teaser/stackchain-season-zero-work-recut.mp4
|
||||
```
|
||||
|
||||
## Input provenance
|
||||
|
||||
- `daily-drop-door-phone.png` is the verified Daily Drop preview at `/root/daily-drop/drops/2026-08-07/the-door-that-refused-a-master-key-phone.png`; SHA-256 `9a72358821414328de129a96461b1f4265be356987f4009037e5ae2f0cab138a`.
|
||||
- `cannon-scene-04.png` is scene 04 from `/root/tiktok-slop-cannon/drops/2026-08-07-1786114450/`; SHA-256 `3080c4135c1595251097387b1e9b8a6022688c3f3ed3db4a919ed9e347d51a15`, matching its source manifest.
|
||||
- The four mutation images were extracted at 5s, 9s, 13s, and 17s from `episode01-four-timmy-mutations.mp4` on branch `timmy/8-four-timmy-mutations`, commit `afa304dc6b5d9d3cc0ede17d6f4f0dfb67fc6b70`; source MP4 SHA-256 `5ddc9a9da1b4b2456faf0b390e5c1be0d3cf48112b417fc075473c90e335d1e4`.
|
||||
- Board, epic, receipt, and test copy are drawn from Gitea issues #7 and #1 and their complete comments read before rendering. The work-board composition is an editorial rendering of that live queue state, not a claim that Gitea supplied a screenshot endpoint.
|
||||
@@ -1,286 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Deterministically build the work-derived Season Zero X teaser."""
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import subprocess
|
||||
import wave
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image, ImageDraw, ImageEnhance, ImageFilter, ImageFont
|
||||
|
||||
W, H, FPS, RATE, DURATION = 720, 1280, 30, 48_000, 30.0
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
SOURCE = ROOT / "source"
|
||||
OUT = ROOT / "stackchain-season-zero-work-recut.mp4"
|
||||
TMP_VIDEO = ROOT / ".video.mp4"
|
||||
TMP_AUDIO = ROOT / ".audio.wav"
|
||||
BOLD = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"
|
||||
REG = "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"
|
||||
COLORS = {"ink": "#070b14", "panel": "#111827", "line": "#334155", "white": "#f8fafc", "muted": "#a9b7cc", "gold": "#ffc857", "cyan": "#5ee7f2", "violet": "#c69cff", "green": "#8ff0a4", "orange": "#ff9c66"}
|
||||
ASSETS = {name: Image.open(SOURCE / filename).convert("RGB") for name, filename in {
|
||||
"drop": "daily-drop-door-phone.png", "cannon": "cannon-scene-04.png",
|
||||
"wizard": "wizard.png", "machine": "machine.png", "creature": "creature.png", "turd": "talking-turd.png",
|
||||
}.items()}
|
||||
|
||||
|
||||
def font(size: int, bold: bool = False):
|
||||
return ImageFont.truetype(BOLD if bold else REG, size)
|
||||
|
||||
|
||||
def ease(x: float) -> float:
|
||||
x = max(0.0, min(1.0, x))
|
||||
return x * x * (3 - 2 * x)
|
||||
|
||||
|
||||
def fit_crop(image: Image.Image, box: tuple[int, int, int, int], zoom: float = 1.0, pan_y: float = 0.5) -> Image.Image:
|
||||
x0, y0, x1, y1 = box
|
||||
bw, bh = x1-x0, y1-y0
|
||||
scale = max(bw/image.width, bh/image.height) * zoom
|
||||
rw, rh = max(1, int(image.width*scale)), max(1, int(image.height*scale))
|
||||
resized = image.resize((rw, rh), Image.Resampling.LANCZOS)
|
||||
left = max(0, (rw-bw)//2)
|
||||
top = max(0, min(rh-bh, int((rh-bh)*pan_y)))
|
||||
return resized.crop((left, top, left+bw, top+bh))
|
||||
|
||||
|
||||
def rounded_image(canvas: Image.Image, image: Image.Image, box: tuple[int, int, int, int], radius: int = 24, zoom: float = 1.0, pan_y: float = 0.5, border: str = COLORS["line"]):
|
||||
crop = fit_crop(image, box, zoom, pan_y)
|
||||
mask = Image.new("L", crop.size, 0)
|
||||
ImageDraw.Draw(mask).rounded_rectangle((0, 0, crop.width-1, crop.height-1), radius, fill=255)
|
||||
canvas.paste(crop, (box[0], box[1]), mask)
|
||||
d = ImageDraw.Draw(canvas)
|
||||
d.rounded_rectangle(box, radius, outline=border, width=3)
|
||||
|
||||
|
||||
def text(d: ImageDraw.ImageDraw, xy, value: str, size: int, fill: str = COLORS["white"], bold: bool = False, anchor: str | None = None):
|
||||
d.text(xy, value, font=font(size, bold), fill=fill, anchor=anchor)
|
||||
|
||||
|
||||
def centered(d, value: str, y: int, size: int, fill: str = COLORS["white"], bold: bool = False):
|
||||
text(d, (W//2, y), value, size, fill, bold, "ma")
|
||||
|
||||
|
||||
def base(t: float) -> Image.Image:
|
||||
y = np.linspace(0, 1, H)[:, None, None]
|
||||
top = np.array((5, 10, 20))[None, None, :]
|
||||
bottom = np.array((22, 15, 40))[None, None, :]
|
||||
grad = top*(1-y) + bottom*y
|
||||
xwave = 5*np.sin(np.linspace(0, 9, W)[None, :, None] + t*.6)
|
||||
arr = np.broadcast_to(grad, (H, W, 3)) + xwave
|
||||
im = Image.fromarray(np.clip(arr, 0, 255).astype(np.uint8), "RGB")
|
||||
d = ImageDraw.Draw(im)
|
||||
for i in range(7):
|
||||
yy = int((t*52 + i*195) % (H+160))-80
|
||||
d.line((0, yy, W, yy-120), fill=(25, 42, 65), width=2)
|
||||
return im
|
||||
|
||||
|
||||
def chrome(d: ImageDraw.ImageDraw, t: float, active: int):
|
||||
d.rectangle((0, 0, W, 76), fill="#080d18")
|
||||
d.line((0, 76, W, 76), fill=COLORS["line"], width=2)
|
||||
text(d, (32, 23), "STACKCHAIN LAB", 25, COLORS["white"], True)
|
||||
text(d, (W-32, 25), "SEASON ZERO", 20, COLORS["gold"], True, "ra")
|
||||
nodes = [(64, "WORK"), (215, "CHALLENGE"), (390, "MUTATE"), (563, "AUDIENCE")]
|
||||
y = 1208
|
||||
d.line((64, y, 656, y), fill=COLORS["line"], width=5)
|
||||
progress = min(1, max(0, t/DURATION))
|
||||
d.line((64, y, 64+592*progress, y), fill=COLORS["cyan"], width=5)
|
||||
for i, (x, label) in enumerate(nodes):
|
||||
c = COLORS["gold"] if i == active else (COLORS["cyan"] if i < active else COLORS["line"])
|
||||
d.ellipse((x-9, y-9, x+9, y+9), fill=c)
|
||||
text(d, (x, y+27), label, 15, c, True, "ma")
|
||||
scan = int((t*155) % (H-160)) + 80
|
||||
d.rectangle((0, scan, W, scan+2), fill="#5ee7f222")
|
||||
|
||||
|
||||
def issue_card(d, box, number, title, owner, state, accent):
|
||||
d.rounded_rectangle(box, 14, fill=COLORS["panel"], outline=accent, width=2)
|
||||
x0, y0, x1, _ = box
|
||||
text(d, (x0+15, y0+13), f"#{number}", 19, accent, True)
|
||||
text(d, (x1-15, y0+15), state.upper(), 16, COLORS["muted"], True, "ra")
|
||||
words = title.split()
|
||||
lines, line = [], ""
|
||||
for word in words:
|
||||
trial = (line+" "+word).strip()
|
||||
if d.textlength(trial, font=font(19, True)) > x1-x0-30:
|
||||
lines.append(line); line = word
|
||||
else: line = trial
|
||||
lines.append(line)
|
||||
for i, ln in enumerate(lines[:2]): text(d, (x0+15, y0+46+i*25), ln, 19, COLORS["white"], True)
|
||||
text(d, (x0+15, y0+104), f"agent:{owner}", 16, accent)
|
||||
|
||||
|
||||
def scene_board(im: Image.Image, t: float):
|
||||
d = ImageDraw.Draw(im)
|
||||
local = t
|
||||
centered(d, "THE WORK IS PUBLIC", 112, 35, COLORS["white"], True)
|
||||
centered(d, "A LIVE QUEUE. A REAL HANDOFF.", 158, 21, COLORS["cyan"], True)
|
||||
sway = int(7*math.sin(local*1.8))
|
||||
d.rounded_rectangle((28+sway, 205, 692+sway, 1015), 24, fill="#0b1220", outline=COLORS["line"], width=3)
|
||||
text(d, (52+sway, 228), "stackchain / stackchain-lab-loop", 20, COLORS["muted"], True)
|
||||
for x, label, c in [(50, "READY", COLORS["cyan"]), (274, "CLAIMED", COLORS["gold"]), (498, "REVIEW", COLORS["green"])]:
|
||||
text(d, (x+sway, 281), label, 17, c, True)
|
||||
issue_card(d, (48+sway, 320, 254+sway, 455), 7, "The Cannon Needs a Pilot", "vincent", "ready", COLORS["violet"])
|
||||
issue_card(d, (48+sway, 475, 254+sway, 610), 13, "Attack the Cannon Chain", "vincent", "ready", COLORS["cyan"])
|
||||
issue_card(d, (274+sway, 320, 480+sway, 455), 16, "Build the X teaser", "timmy", "claimed", COLORS["gold"])
|
||||
issue_card(d, (498+sway, 320, 672+sway, 455), 1, "Prove the Lab Loop", "vincent", "review", COLORS["green"])
|
||||
d.rounded_rectangle((54+sway, 670, 666+sway, 962), 18, fill="#16112a", outline=COLORS["violet"], width=3)
|
||||
text(d, (78+sway, 694), "EPIC #7 // SEASON ZERO", 21, COLORS["violet"], True)
|
||||
text(d, (78+sway, 744), "THE CANNON NEEDS A PILOT", 28, COLORS["white"], True)
|
||||
text(d, (78+sway, 801), "6-episode character evolution chain", 19, COLORS["muted"])
|
||||
for i, line in enumerate(("✓ persistent Timmy", "✓ power / scar / cost / receipt", "✓ audience choice advances canon")):
|
||||
text(d, (90+sway, 846+i*33), line, 19, COLORS["green"] if i < 2 else COLORS["gold"], True)
|
||||
text(d, (50, 1060), "TIMMY → VINCENT → HUMAN REVIEW", 24, COLORS["gold"], True)
|
||||
|
||||
|
||||
def scene_receipt(im: Image.Image, t: float):
|
||||
d = ImageDraw.Draw(im)
|
||||
local = t-5
|
||||
centered(d, "CHALLENGED. THEN VERIFIED.", 116, 31, COLORS["white"], True)
|
||||
x = 38 + int(8*math.sin(local*2))
|
||||
d.rounded_rectangle((x, 205, W-38+x-38, 945), 22, fill="#0c1423", outline=COLORS["cyan"], width=3)
|
||||
text(d, (x+24, 230), "ISSUE #1 / CONTRIBUTION RECEIPT", 19, COLORS["cyan"], True)
|
||||
d.rounded_rectangle((x+22, 278, W-62, 520), 18, fill=COLORS["panel"], outline=COLORS["line"], width=2)
|
||||
text(d, (x+44, 302), "VINCENT", 23, COLORS["violet"], True)
|
||||
text(d, (W-84, 307), "INDEPENDENT PASS", 16, COLORS["muted"], True, "ra")
|
||||
text(d, (x+44, 355), "Fetched PR #4 from Timmy's branch", 20, COLORS["white"])
|
||||
text(d, (x+44, 397), "8 TESTS PASS", 39, COLORS["green"], True)
|
||||
text(d, (x+44, 452), "Label transition regression verified", 20, COLORS["muted"])
|
||||
d.rounded_rectangle((x+22, 555, W-62, 804), 18, fill="#17122b", outline=COLORS["violet"], width=2)
|
||||
text(d, (x+44, 580), "DEFINITION OF DONE", 19, COLORS["violet"], True)
|
||||
checks = ("Both agents read assigned tickets", "Both leave verified evidence", "Timmy → Vincent → review", "Credentials remain host-local")
|
||||
for i, line in enumerate(checks):
|
||||
text(d, (x+48, 633+i*40), "✓", 22, COLORS["green"], True)
|
||||
text(d, (x+82, 635+i*40), line, 19, COLORS["white"])
|
||||
text(d, (x+24, 856), "RECEIPT: ISSUE #1 • PR #4 • 8/8", 21, COLORS["gold"], True)
|
||||
centered(d, "CRITIQUE BECOMES A BUILD INPUT.", 1012, 23, COLORS["gold"], True)
|
||||
centered(d, "NOT A COMMENT. A CONSEQUENCE.", 1055, 19, COLORS["muted"], True)
|
||||
|
||||
|
||||
def scene_sources(im: Image.Image, t: float):
|
||||
d = ImageDraw.Draw(im)
|
||||
local = t-10
|
||||
centered(d, "REAL ARTIFACTS ENTER THE CHAIN", 112, 29, COLORS["white"], True)
|
||||
shift = int(10*math.sin(local*.9))
|
||||
rounded_image(im, ASSETS["drop"], (35+shift, 205, 350+shift, 930), 22, 1.03+local*.008, .46, COLORS["gold"])
|
||||
rounded_image(im, ASSETS["cannon"], (370-shift, 205, 685-shift, 930), 22, 1.05+local*.01, .5, COLORS["orange"])
|
||||
d = ImageDraw.Draw(im)
|
||||
d.rounded_rectangle((48+shift, 835, 337+shift, 913), 12, fill="#070b14dd")
|
||||
text(d, (65+shift, 852), "DAILY DROP", 19, COLORS["gold"], True)
|
||||
text(d, (65+shift, 881), "THE DOOR / VERIFIED", 16, COLORS["white"], True)
|
||||
d.rounded_rectangle((383-shift, 835, 672-shift, 913), 12, fill="#070b14dd")
|
||||
text(d, (400-shift, 852), "CANNON OUTPUT", 19, COLORS["orange"], True)
|
||||
text(d, (400-shift, 881), "SCENE 04 / SOURCE", 16, COLORS["white"], True)
|
||||
# Animated transfer beam unifies the two work artifacts.
|
||||
beam = int(95 + (local % 2.0)/2.0*520)
|
||||
d.line((beam, 965, beam+70, 965), fill=COLORS["cyan"], width=7)
|
||||
d.polygon([(beam+70, 953), (beam+94, 965), (beam+70, 977)], fill=COLORS["cyan"])
|
||||
centered(d, "SOURCE → PRESSURE → MUTATION", 1025, 23, COLORS["cyan"], True)
|
||||
centered(d, "THE ART CHANGES JOBS. IT ISN'T FILLER.", 1070, 18, COLORS["muted"], True)
|
||||
|
||||
|
||||
def scene_mutations(im: Image.Image, t: float):
|
||||
d = ImageDraw.Draw(im)
|
||||
local = t-16
|
||||
centered(d, "ONE PILOT. FOUR CONSEQUENCES.", 108, 29, COLORS["white"], True)
|
||||
specs = [
|
||||
("wizard", "WIZARD", "AMBIGUITY → SPELL", COLORS["violet"]),
|
||||
("machine", "MACHINE", "REPEATABILITY → EXACT", COLORS["cyan"]),
|
||||
("creature", "CREATURE", "HUNGER → SIGNAL", COLORS["green"]),
|
||||
("turd", "TALKING TURD", "RIDICULE → PUNCHLINE", "#ffd18a"),
|
||||
]
|
||||
boxes = [(35, 190, 350, 610), (370, 190, 685, 610), (35, 650, 350, 1070), (370, 650, 685, 1070)]
|
||||
for i, ((key, name, pressure, color), box) in enumerate(zip(specs, boxes)):
|
||||
reveal = ease((local-i*.55)/1.3)
|
||||
cx = (box[0]+box[2])//2
|
||||
cy = (box[1]+box[3])//2
|
||||
bw, bh = box[2]-box[0], box[3]-box[1]
|
||||
sw, sh = max(8, int(bw*reveal)), max(8, int(bh*reveal))
|
||||
live = (cx-sw//2, cy-sh//2, cx+sw//2, cy+sh//2)
|
||||
rounded_image(im, ASSETS[key], live, 20, 1.0+.025*math.sin(local*1.7+i), .45, color)
|
||||
d = ImageDraw.Draw(im)
|
||||
if reveal > .78:
|
||||
alpha_box = (box[0]+8, box[3]-94, box[2]-8, box[3]-10)
|
||||
d.rounded_rectangle(alpha_box, 12, fill="#070b14e8")
|
||||
text(d, (box[0]+20, box[3]-82), name, 20 if name != "TALKING TURD" else 17, color, True)
|
||||
text(d, (box[0]+20, box[3]-48), pressure, 16, COLORS["white"], True)
|
||||
centered(d, "PLANNED CARDS • EPISODE 01", 1120, 18, COLORS["gold"], True)
|
||||
|
||||
|
||||
def scene_gate(im: Image.Image, t: float):
|
||||
d = ImageDraw.Draw(im)
|
||||
local = t-25
|
||||
for i, color in enumerate((COLORS["violet"], COLORS["cyan"], COLORS["green"], "#ffd18a")):
|
||||
a = local*.8+i*math.pi/2
|
||||
x, y = W/2+math.cos(a)*245, 390+math.sin(a)*130
|
||||
r = 50+8*math.sin(local*3+i)
|
||||
d.ellipse((x-r, y-r, x+r, y+r), fill=color, outline=COLORS["white"], width=3)
|
||||
centered(d, "THE AUDIENCE ALTERS CANON", 125, 25, COLORS["gold"], True)
|
||||
centered(d, "WHICH TIMMY", 590, 57, COLORS["white"], True)
|
||||
centered(d, "GETS TO REMEMBER", 675, 49, COLORS["white"], True)
|
||||
centered(d, "THIS?", 765, 76, COLORS["gold"], True)
|
||||
d.rounded_rectangle((55, 892, W-55, 1000), 18, outline=COLORS["cyan"], width=3)
|
||||
centered(d, "WIZARD / MACHINE", 917, 24, COLORS["white"], True)
|
||||
centered(d, "CREATURE / TALKING TURD", 959, 21, COLORS["white"], True)
|
||||
centered(d, "NO POST UNTIL HUMAN APPROVAL", 1072, 19, COLORS["muted"], True)
|
||||
|
||||
|
||||
def frame(t: float) -> Image.Image:
|
||||
im = base(t)
|
||||
if t < 5: scene_board(im, t); active = 0
|
||||
elif t < 10: scene_receipt(im, t); active = 1
|
||||
elif t < 16: scene_sources(im, t); active = 1
|
||||
elif t < 25: scene_mutations(im, t); active = 2
|
||||
else: scene_gate(im, t); active = 3
|
||||
chrome(ImageDraw.Draw(im), t, active)
|
||||
# Soft entrance/exit only; scene motion remains continuous within each stage.
|
||||
if t < .35:
|
||||
im = ImageEnhance.Brightness(im).enhance(ease(t/.35))
|
||||
if t > DURATION-.5:
|
||||
im = ImageEnhance.Brightness(im).enhance(max(0, (DURATION-t)/.5))
|
||||
return im
|
||||
|
||||
|
||||
def build_audio():
|
||||
n = int(DURATION*RATE)
|
||||
audio = np.zeros(n, dtype=np.float64)
|
||||
rng = np.random.default_rng(1608072026)
|
||||
for beat in np.arange(0, DURATION, .5):
|
||||
i = int(beat*RATE); length = min(int(.16*RATE), n-i); x = np.arange(length)/RATE
|
||||
audio[i:i+length] += .42*np.sin(2*np.pi*(74*x-28*x*x))*np.exp(-x*25)
|
||||
for beat in np.arange(.5, DURATION, 1):
|
||||
i = int(beat*RATE); length = min(int(.12*RATE), n-i); x = np.arange(length)/RATE
|
||||
audio[i:i+length] += .12*rng.normal(0, 1, length)*np.exp(-x*34)
|
||||
for beat in np.arange(.25, DURATION, .5):
|
||||
i = int(beat*RATE); length = min(int(.035*RATE), n-i); x = np.arange(length)/RATE
|
||||
audio[i:i+length] += .04*rng.normal(0, 1, length)*np.exp(-x*80)
|
||||
stages = [(0, 5, (110, 165, 220)), (5, 10, (123, 185, 247)), (10, 16, (98, 147, 196)), (16, 25, (131, 196, 262)), (25, 30, (110, 220, 330))]
|
||||
for start, end, chord in stages:
|
||||
i0, i1 = int(start*RATE), int(end*RATE); x = np.arange(i1-i0)/RATE
|
||||
env = np.minimum(1, x*2)*np.minimum(1, (end-start-x)*2)
|
||||
for f in chord: audio[i0:i1] += .035*np.sin(2*np.pi*f*x)*env
|
||||
audio = np.tanh(audio*1.4)
|
||||
pcm = (audio*32767).astype("<i2")
|
||||
with wave.open(str(TMP_AUDIO), "wb") as w:
|
||||
w.setnchannels(1); w.setsampwidth(2); w.setframerate(RATE); w.writeframes(pcm.tobytes())
|
||||
|
||||
|
||||
def main():
|
||||
cmd = ["ffmpeg", "-y", "-loglevel", "error", "-f", "rawvideo", "-pix_fmt", "rgb24", "-s", f"{W}x{H}", "-r", str(FPS), "-i", "-", "-an", "-c:v", "libx264", "-preset", "medium", "-crf", "20", "-pix_fmt", "yuv420p", "-movflags", "+faststart", str(TMP_VIDEO)]
|
||||
proc = subprocess.Popen(cmd, stdin=subprocess.PIPE)
|
||||
assert proc.stdin
|
||||
try:
|
||||
for i in range(int(DURATION*FPS)): proc.stdin.write(frame(i/FPS).tobytes())
|
||||
finally: proc.stdin.close()
|
||||
if proc.wait(): raise SystemExit("video encode failed")
|
||||
build_audio()
|
||||
subprocess.run(["ffmpeg", "-y", "-loglevel", "error", "-i", str(TMP_VIDEO), "-i", str(TMP_AUDIO), "-c:v", "copy", "-c:a", "aac", "-b:a", "160k", "-shortest", "-movflags", "+faststart", str(OUT)], check=True)
|
||||
TMP_VIDEO.unlink(); TMP_AUDIO.unlink()
|
||||
for sec, name in [(2, "01-board"), (7, "02-vincent-receipt"), (13, "03-source-artifacts"), (21, "04-mutations"), (27, "05-audience-gate")]:
|
||||
frame(sec).save(ROOT / "stills" / f"{name}.jpg", quality=88)
|
||||
print(OUT)
|
||||
|
||||
|
||||
if __name__ == "__main__": main()
|
||||
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 205 KiB |
|
Before Width: | Height: | Size: 188 KiB |
|
Before Width: | Height: | Size: 150 KiB |
|
Before Width: | Height: | Size: 174 KiB |
|
Before Width: | Height: | Size: 183 KiB |
|
Before Width: | Height: | Size: 133 KiB |
|
Before Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 201 KiB |
|
Before Width: | Height: | Size: 140 KiB |
|
Before Width: | Height: | Size: 104 KiB |
@@ -0,0 +1,75 @@
|
||||
# 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
|
||||
|
||||
- 20–35 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?**
|
||||
@@ -0,0 +1,45 @@
|
||||
# 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.
|
||||
@@ -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": []
|
||||
}
|
||||
@@ -0,0 +1,327 @@
|
||||
"""
|
||||
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 _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", "power_delta", "scar_delta", "cost_delta"):
|
||||
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")
|
||||
|
||||
# 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["signed_by"]
|
||||
if 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")
|
||||
|
||||
# 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 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 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)
|
||||
@@ -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()
|
||||
@@ -0,0 +1,414 @@
|
||||
"""
|
||||
Fail-closed validator tests for the revision-2 canon lifecycle contract.
|
||||
|
||||
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
|
||||
|
||||
CANON = Path(__file__).parents[1] / "projects" / "slop-cannon" / "canon.json"
|
||||
VALIDATOR_PATH = Path(__file__).parents[1] / "projects" / "slop-cannon" / "canon_validator.py"
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
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": "Added lifecycle state tracking",
|
||||
"scar_delta": "Receipts now enforce structured provenance",
|
||||
"cost_delta": "Every drop requires a signed, bound receipt",
|
||||
"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):
|
||||
result = cv.validate_canon(_valid_canon())
|
||||
self.assertTrue(result["valid"], result["errors"])
|
||||
|
||||
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("schema_version" in e for e in result["errors"]))
|
||||
|
||||
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("schema_version" in e and "unsupported" in e for e in result["errors"]))
|
||||
|
||||
|
||||
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("action" in e for e in result["errors"]))
|
||||
|
||||
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("power_delta" in e for e in result["errors"]))
|
||||
|
||||
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("scar_delta" in e for e in result["errors"]))
|
||||
|
||||
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("cost_delta" in e for e in result["errors"]))
|
||||
|
||||
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("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):
|
||||
data = _valid_canon() # all candidates
|
||||
data["audience_choice"] = {
|
||||
"survivor_id": "wizard",
|
||||
"stolen_trait": "Proves the execution path",
|
||||
"source_episode": "01-pilot",
|
||||
"source_issue": 19,
|
||||
"committed_at": "2026-08-14T18:00:00Z",
|
||||
}
|
||||
result = cv.validate_canon(data)
|
||||
self.assertFalse(result["valid"])
|
||||
self.assertTrue(any("survivor" in e for e in result["errors"]))
|
||||
|
||||
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("archive_ghost" in e for e in result["errors"]))
|
||||
|
||||
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):
|
||||
data = _valid_canon()
|
||||
data["forms"].append(copy.deepcopy(data["forms"][0]))
|
||||
result = cv.validate_canon(data)
|
||||
self.assertFalse(result["valid"])
|
||||
self.assertTrue(any("duplicate form id" in e for e in result["errors"]))
|
||||
|
||||
def test_short_palette_fails(self):
|
||||
data = _valid_canon()
|
||||
data["forms"][0]["palette"] = ["red"]
|
||||
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 DailyReceiptFixtureTests(unittest.TestCase):
|
||||
"""Daily Lab receipt fixtures with structured provenance binding."""
|
||||
|
||||
def test_vincent_receipt_fixture(self):
|
||||
data = _valid_canon()
|
||||
data["receipts"] = [_valid_receipt()]
|
||||
result = cv.validate_canon(data)
|
||||
self.assertTrue(result["valid"], result["errors"])
|
||||
|
||||
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"])
|
||||
|
||||
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()
|
||||