feat(kvb-coach-meditation): short alpha-wave meditation with fractals, tuned by feedback
kvb-coach-meditation writes a local session page with its settings embedded (xdg-open drops a file:// query): generative pentatonic ambience with reverb over an isochronic or binaural alpha layer (8-13 Hz), a WebGL Julia fractal that zooms with the breathing cue (box, 4-7-8 or none), breath-synced glowing type in URW Gothic, +/- keys for the picture's brightness kept in localStorage, and a progress line along the bottom edge. meditation.py validates every setting and refuses a session while one is missing; after every session kvb-think-listen collects feedback that becomes one set per answer, logged in the kvb-coach profile. Adds the category coach: every coaching skill beyond kvb-coach is named kvb-coach-<name>. tests/test_meditation.py keeps script and page in step. Operator order 2026-11-03; two descriptions trimmed for the listing budget. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
57c3322076
commit
5f30644ca6
9 files changed
+541
-12
No files matched your search
@@ -0,0 +1,88 @@
|
||||
"""Exercise meditation.py and keep it in step with the keys and choices of meditation.html."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
SKILL = REPO_ROOT / "skills" / "kvb-coach-meditation"
|
||||
SCRIPT = SKILL / "scripts" / "meditation.py"
|
||||
PAGE = SKILL / "assets" / "meditation.html"
|
||||
FULL = [
|
||||
"minutes=5",
|
||||
"beat=10",
|
||||
"base=196",
|
||||
"volume=0.4",
|
||||
"mode=isochronic",
|
||||
"palette=aurora",
|
||||
"breathing=box",
|
||||
]
|
||||
|
||||
|
||||
class TestMeditation(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.tmp.cleanup)
|
||||
self.state = Path(self.tmp.name) / "state" / "meditation.json"
|
||||
|
||||
def _run(self, *args: str) -> subprocess.CompletedProcess:
|
||||
return subprocess.run(
|
||||
[sys.executable, str(SCRIPT), "--state", str(self.state), *args],
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
def _session(self) -> subprocess.CompletedProcess:
|
||||
out = Path(self.tmp.name) / "session.html"
|
||||
return self._run("session", "--page", str(PAGE), "--out", str(out))
|
||||
|
||||
def test_session_page_embeds_every_setting(self):
|
||||
self.assertEqual(self._run("set", *FULL).returncode, 0)
|
||||
result = self._session()
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
page = Path(result.stdout.strip()).read_text(encoding="utf-8")
|
||||
self.assertNotIn("const SETTINGS = null;", page)
|
||||
embedded = json.loads(re.search(r"const SETTINGS = (\{.*?\});", page).group(1))
|
||||
self.assertEqual(embedded["beat"], 10.0)
|
||||
self.assertEqual(embedded["minutes"], 5)
|
||||
self.assertEqual(set(embedded), {pair.split("=")[0] for pair in FULL})
|
||||
|
||||
def test_session_refuses_missing_settings(self):
|
||||
self._run("set", "minutes=5")
|
||||
self.assertEqual(self._session().returncode, 2)
|
||||
self.assertFalse((Path(self.tmp.name) / "session.html").exists())
|
||||
|
||||
def test_invalid_values_exit_2_and_keep_the_state(self):
|
||||
self._run("set", *FULL)
|
||||
before = json.loads(self.state.read_text(encoding="utf-8"))
|
||||
for pair in ("beat=20", "mode=loud", "minutes=five", "tempo=3", "volume"):
|
||||
with self.subTest(pair=pair):
|
||||
self.assertEqual(self._run("set", "minutes=7", pair).returncode, 2)
|
||||
self.assertEqual(json.loads(self.state.read_text(encoding="utf-8")), before)
|
||||
|
||||
|
||||
class TestPageMatchesScript(unittest.TestCase):
|
||||
def test_keys_and_choices_agree(self):
|
||||
spec = importlib.util.spec_from_file_location("meditation", SCRIPT)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
page = PAGE.read_text(encoding="utf-8")
|
||||
self.assertEqual(page.count(module.PLACEHOLDER), 1)
|
||||
keys = re.search(r"const KEYS = \[(.*?)\];", page).group(1)
|
||||
self.assertEqual(re.findall(r'"(\w+)"', keys), list(module.KEYS))
|
||||
for name, choices in (("PALETTES", "palette"), ("PATTERNS", "breathing")):
|
||||
block = re.search(rf"const {name} = \{{(.*?)\n\}};", page, re.DOTALL).group(1)
|
||||
found = re.findall(r'^\s*"?(\w+)"?:', block, re.MULTILINE)
|
||||
self.assertEqual(sorted(found), sorted(module.CHOICES[choices]))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -9,9 +9,16 @@ from pathlib import Path
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
SKILLS = REPO_ROOT / "skills"
|
||||
README = REPO_ROOT / "README.md"
|
||||
CATEGORIES = ("think", "create", "code", "ci", "git", "check", "org", "skill")
|
||||
CATEGORIES = ("think", "create", "code", "ci", "git", "check", "org", "skill", "coach")
|
||||
UNCATEGORIZED = frozenset(
|
||||
{"kvb-cleanup", "kvb-coach", "kvb-dream", "kvb-help", "kvb-optimize", "kvb-transcend"}
|
||||
{
|
||||
"kvb-cleanup",
|
||||
"kvb-coach",
|
||||
"kvb-dream",
|
||||
"kvb-help",
|
||||
"kvb-optimize",
|
||||
"kvb-transcend",
|
||||
}
|
||||
)
|
||||
SPEC = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$")
|
||||
SCHEMA = re.compile(rf"^kvb-(?:{'|'.join(CATEGORIES)})-[a-z0-9]+(?:-[a-z0-9]+)*$")
|
||||
|
||||
Reference in new issue
Block a user