Four shipped patches on a read-only shelf: ✦ the heartbeat (boom-bap, mpc 8-4, kick duck), ✦ the wheel within the wheel (WHEELS polymeter — kick 16 / hat 12 / clap 10), ✦ the squeeze (GODSQUASH — the market & quakes pump the master), ✦ quiet arrival (gentle earth-echo ambient, no drums). git-tracked in factory/, authored in the client's serializePatch shape. Course-correction from the brief's stale anchor: the template select reads /api/presets (account-based, auth.py), NOT the hub's legacy patchList — so the factory shelf is injected there. list_factory() + load_factory() prepend the ✦ patches for every signed-in player; PUT/DELETE on a ✦ name → 403 (read-only); a missing factory/ degrades to []. test_factory.py (deploy-excluded) validates shape. Verified: HTTP dispatch serves the 4 ✦ names leading the list, loads one, rejects writes (403), user saves still work, traversal/missing → None, zero index.html changes. Taste disclaimer: these are structural drafts — correct shape is the deliverable, beauty is the human's. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
92 lines
3.8 KiB
Python
92 lines
3.8 KiB
Python
#!/usr/bin/env python3
|
|
"""test_factory.py — validate the ✦ CANON factory patches' SHAPE.
|
|
|
|
Not a taste check (the human re-voices by ear) — this asserts every factory
|
|
patch loads cleanly through the client's migrateSeq expectations: 16-step seq
|
|
arrays, wlen in 1..16, sane bpm, valid route/groove/scale shape, and a
|
|
✦-prefixed name. Stdlib only; excluded from the deploy by the test_*.py filter.
|
|
|
|
python3 test_factory.py
|
|
"""
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
FACTORY = os.path.join(HERE, "factory")
|
|
SCALES = {"chromatic", "major", "minor", "dorian", "phrygian", "lydian",
|
|
"mixolydian", "pentatonic", "minor_pent", "wholetone", "hirajoshi"}
|
|
SEQ_ARRAYS = ("steps", "vel", "chance", "rat", "lock", "len")
|
|
|
|
|
|
def check_seq(key, s, errs):
|
|
for a in SEQ_ARRAYS:
|
|
if a in s:
|
|
if not isinstance(s[a], list) or len(s[a]) != 16:
|
|
errs.append(f"{key}.{a} must be a 16-length array")
|
|
if "steps" not in s or not isinstance(s.get("steps"), list) or len(s["steps"]) != 16:
|
|
errs.append(f"{key}.steps missing/not-16 (migrateSeq would reject → dropped)")
|
|
if "wlen" in s and not (isinstance(s["wlen"], int) and 1 <= s["wlen"] <= 16):
|
|
errs.append(f"{key}.wlen must be an int 1..16, got {s.get('wlen')!r}")
|
|
if "on" in s and not isinstance(s["on"], (bool, int)):
|
|
errs.append(f"{key}.on must be boolean-ish")
|
|
|
|
|
|
def check_patch(name, p, errs):
|
|
if not isinstance(p.get("bpm"), (int, float)) or not (40 <= p["bpm"] <= 180):
|
|
errs.append(f"bpm must be 40..180, got {p.get('bpm')!r}")
|
|
for r in p.get("routes", []):
|
|
if not r.get("source") or not r.get("dest"):
|
|
errs.append("route missing source/dest")
|
|
if not isinstance(r.get("amount"), (int, float)):
|
|
errs.append(f"route {r.get('source')}→{r.get('dest')} amount not numeric")
|
|
for key, s in (p.get("seqs") or {}).items():
|
|
check_seq(key, s, errs)
|
|
sc = p.get("scale") or {}
|
|
if sc and sc.get("name") not in SCALES:
|
|
errs.append(f"scale.name {sc.get('name')!r} not a known scale")
|
|
gr = p.get("groove") or {}
|
|
for k in ("swing", "humanize", "velo"):
|
|
if k in gr and not (0 <= gr[k] <= 1):
|
|
errs.append(f"groove.{k} out of 0..1")
|
|
|
|
|
|
def main():
|
|
if not os.path.isdir(FACTORY):
|
|
print("FAIL: no factory/ dir"); sys.exit(1)
|
|
files = sorted(f for f in os.listdir(FACTORY) if f.endswith(".json"))
|
|
if len(files) < 4:
|
|
print(f"FAIL: expected ≥4 factory patches, found {len(files)}"); sys.exit(1)
|
|
total_errs = 0
|
|
names = []
|
|
for fn in files:
|
|
with open(os.path.join(FACTORY, fn), encoding="utf-8") as f:
|
|
obj = json.load(f)
|
|
name, data = obj.get("name", ""), obj.get("data")
|
|
errs = []
|
|
if not name.startswith("✦ "):
|
|
errs.append(f"name {name!r} must start with '✦ '")
|
|
if not isinstance(data, dict):
|
|
errs.append("missing 'data' object")
|
|
else:
|
|
check_patch(name, data, errs)
|
|
names.append(name)
|
|
if errs:
|
|
total_errs += len(errs)
|
|
print(f" ✗ {fn}: " + "; ".join(errs))
|
|
else:
|
|
nseq = len(data.get("seqs") or {})
|
|
nroute = len(data.get("routes") or [])
|
|
print(f" ✓ {name} (bpm {data['bpm']}, {nseq} seqs, {nroute} routes)")
|
|
# the four the brief names
|
|
for want in ("✦ the heartbeat", "✦ the wheel within the wheel", "✦ the squeeze", "✦ quiet arrival"):
|
|
if want not in names:
|
|
print(f" ✗ missing canonical patch: {want}"); total_errs += 1
|
|
if total_errs:
|
|
print(f"\nFAIL: {total_errs} shape error(s)"); sys.exit(1)
|
|
print(f"\nALL {len(files)} FACTORY PATCHES VALID (shape only — beauty is the human's)")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|