🌱 LIFESTRUMENT — the gentle client (viz/life.html)

A second, self-contained client for the same hub, mobile-first: only
the sources about living (bodies, births, weather, sun & moon, play),
eight one-tap Vibes instead of a matrix, two macro knobs (ENERGY /
DREAM), a breathing orb, and dopamine Moments (somewhere a baby, the
sun where you stand, a kickoff). Its own compact Web Audio synth, all
routing client-side so it plays against the read-only public hub.
Reuses the invite-only /api auth; local sensors (heart BT, device tilt,
shake, mic, tap) never touch the hub. No money, no machinery, no
cables. Links back to the full instrument; added a "🌱 lifestrument"
item to godstrument's sky menu. Built understand→build→review with an
adversarial pass; 21 findings fixed (a11y sliders, zoom, contrast,
warm-dawn background, reachable heart connect, pluck de-click,
scheduler catch-up cap, moment tuning). test_lifestrument.py guards the
whitelist and the money/machinery ban.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-12 13:43:48 +10:00
parent dab04294ef
commit 350ad72782
3 changed files with 1269 additions and 0 deletions

117
test_lifestrument.py Normal file
View File

@ -0,0 +1,117 @@
#!/usr/bin/env python3
"""Self-check for viz/life.html (Lifestrument). Stdlib only, no deps.
Asserts the three things that keep the fun page honest:
1. life.html parses as balanced HTML (well-formed tags).
2. every hub source it wires is a real config.json signal (or a local me.* one).
3. the banned families (money & machinery) appear NOWHERE in the file.
Run: python3 test_lifestrument.py
"""
import json
import os
import re
import sys
from html.parser import HTMLParser
HERE = os.path.dirname(os.path.abspath(__file__))
LIFE = os.path.join(HERE, "viz", "life.html")
CONFIG = os.path.join(HERE, "config.json")
BANNED_PREFIXES = ("crypto.", "fx.", "market.", "econ.", "debt.", "quake.", "planes.", "iss.")
# void elements never need a close tag
VOID = {"area", "base", "br", "col", "embed", "hr", "img", "input", "link",
"meta", "param", "source", "track", "wbr"}
class Balance(HTMLParser):
def __init__(self):
super().__init__()
self.stack = []
self.errors = []
def handle_starttag(self, tag, attrs):
if tag not in VOID:
self.stack.append(tag)
def handle_startendtag(self, tag, attrs):
pass # self-closing, balanced by definition
def handle_endtag(self, tag):
if tag in VOID:
return
if not self.stack:
self.errors.append(f"stray </{tag}>")
return
if self.stack[-1] == tag:
self.stack.pop()
elif tag in self.stack:
# close intervening (lenient, like a browser) but flag it
while self.stack and self.stack[-1] != tag:
self.stack.pop()
if self.stack:
self.stack.pop()
else:
self.errors.append(f"unexpected </{tag}>")
def test_balanced_html(html):
p = Balance()
p.feed(html)
assert not p.errors, f"HTML tag errors: {p.errors}"
assert not p.stack, f"unclosed tags: {p.stack}"
print(" ok: HTML is balanced")
def test_no_banned(html):
hits = []
for pref in BANNED_PREFIXES:
# word-ish boundary so 'econ.' doesn't match e.g. 'second.' — require a
# quote/backtick/space before the prefix, as our source keys always are.
for m in re.finditer(r'["\'`\s(]' + re.escape(pref), html):
frag = html[m.start():m.start() + 40].replace("\n", " ")
hits.append(f"{pref!r} near: …{frag}")
assert not hits, "banned money/machinery keys present:\n " + "\n ".join(hits)
print(f" ok: none of {BANNED_PREFIXES} appear")
def test_whitelist_valid(html):
signals = set(json.load(open(CONFIG))["signals"].keys())
# pull the WHITELIST Set literal from the source
m = re.search(r"const WHITELIST = new Set\(\[(.*?)\]\)", html, re.S)
assert m, "could not find WHITELIST Set in life.html"
keys = re.findall(r'"([^"]+)"', m.group(1))
assert len(keys) >= 20, f"whitelist suspiciously small: {len(keys)} keys"
missing = [k for k in keys if k not in signals]
assert not missing, f"whitelist keys not in config.json signals: {missing}"
# none of the whitelisted keys may themselves be banned
bad = [k for k in keys if k.startswith(BANNED_PREFIXES)]
assert not bad, f"whitelist contains banned keys: {bad}"
print(f" ok: all {len(keys)} whitelist keys exist in config.json, none banned")
def test_reuses_real_endpoints(html):
for ep in ("/api/login", "/api/signup", "/api/me", "/api/logout"):
assert ep in html, f"missing expected auth endpoint {ep}"
# exact field names the hub expects
assert "identifier:" in html, "login must send {identifier, password}"
assert "invite:" in html, "signup must send an invite code"
print(" ok: reuses the real /api/* auth endpoints with correct fields")
def main():
html = open(LIFE, encoding="utf-8").read()
print("test_lifestrument.py — checking viz/life.html")
test_balanced_html(html)
test_no_banned(html)
test_whitelist_valid(html)
test_reuses_real_endpoints(html)
print("ALL CHECKS PASSED")
if __name__ == "__main__":
try:
main()
except AssertionError as e:
print("FAIL:", e)
sys.exit(1)

View File

@ -6642,6 +6642,7 @@
: "🌐 tab feed cancelled"; : "🌐 tab feed cancelled";
eventTicker.unshift({ text: msg, tAdded: now(), src: "sys" }); }); eventTicker.unshift({ text: msg, tAdded: now(), src: "sys" }); });
ctxItem("⌨ keys — remap the keyboard", () => openKeys()); ctxItem("⌨ keys — remap the keyboard", () => openKeys());
ctxItem("🌱 lifestrument — the gentle one (new tab)", () => window.open("/life.html", "_blank"));
if (commune.room) { if (commune.room) {
const gOwner = commune.alloc["@godtime"]; const gOwner = commune.alloc["@godtime"];
ctxItem(gOwner === communeMe() ? "🕸 release the godtime" : ctxItem(gOwner === communeMe() ? "🕸 release the godtime" :

1151
viz/life.html Normal file

File diff suppressed because it is too large Load Diff