From 254c8ce2409b9365b8c09a8aa5eef3f0eb993725 Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Sun, 1 Nov 2026 19:05:04 +0100 Subject: [PATCH] Add design conformance counter, score TODO Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01WzxdWXTUM9dpnCS2wtFkx4 --- CLAUDE.md | 4 +- music_theory/TODO-multivoice-score.md | 31 +++ unified-design-system/scripts/conformance.py | 206 ++++++++++++++++++ .../scripts/conformance_diff.py | 74 +++++++ unified-design-system/tests/conftest.py | 6 + .../tests/test_conformance.py | 118 ++++++++++ .../tests/test_conformance_diff.py | 71 ++++++ 7 files changed, 508 insertions(+), 2 deletions(-) create mode 100644 music_theory/TODO-multivoice-score.md create mode 100644 unified-design-system/scripts/conformance.py create mode 100644 unified-design-system/scripts/conformance_diff.py create mode 100644 unified-design-system/tests/conftest.py create mode 100644 unified-design-system/tests/test_conformance.py create mode 100644 unified-design-system/tests/test_conformance_diff.py diff --git a/CLAUDE.md b/CLAUDE.md index 495e19e..f0844ee 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -11,8 +11,8 @@ overwrite it with `repo_contract/templates/check_repo_contract.shim.sh`. ## Commands - run: n/a: collection of gates and libraries, no single entry point -- test: `PYTHONPATH=. python -m pytest -q agent_ready/tests coverage_gaps/tests dep_freshness/tests file_length/tests flutter_web_drive/tests home_paths/tests md_naming/tests repo_contract/tests scripts/tests` +- test: `PYTHONPATH=. python -m pytest -q agent_ready/tests coverage_gaps/tests dep_freshness/tests file_length/tests flutter_web_drive/tests home_paths/tests md_naming/tests repo_contract/tests scripts/tests unified-design-system/tests` - test-changed: `scripts/test_changed.sh` - lint: `ruff check agent_ready coverage_gaps flutter_web_drive repo_contract` -- coverage: `PYTHONPATH=. python -m pytest -q agent_ready/tests flutter_web_drive/tests repo_contract/tests --cov=agent_ready --cov=flutter_web_drive --cov=repo_contract --cov-branch --cov-report=xml --cov-fail-under=100` +- coverage: `PYTHONPATH=. python -m pytest -q agent_ready/tests flutter_web_drive/tests repo_contract/tests unified-design-system/tests --cov=agent_ready --cov=flutter_web_drive --cov=repo_contract --cov=conformance --cov=conformance_diff --cov-branch --cov-report=xml --cov-fail-under=100` - coverage-gaps: `coverage-gaps coverage.xml` diff --git a/music_theory/TODO-multivoice-score.md b/music_theory/TODO-multivoice-score.md new file mode 100644 index 0000000..8699131 --- /dev/null +++ b/music_theory/TODO-multivoice-score.md @@ -0,0 +1,31 @@ +# Add a plain-text multi-voice score format + +## what +Add a plain-text score format that `compose.py` and `sequencer.py` can +render: named voices, a noise-drum voice, a `meter` field (3/4, 6/8) and +A/B sections. Today `sequencer.BEATS_PER_BAR` is fixed at 4 and a `Recipe` +has one bass, one arpeggio and one melody. +Source: simonwillison.net/2026/Oct/6/scrimshaw-jukebox. + +## done +``` +cd ~/src/utils/music_theory && PYTHONPATH=. python -m pytest music_theory/tests/test_score.py music_theory/tests/test_sequencer.py music_theory/tests/test_compose.py --cov=music_theory.score --cov=music_theory.sequencer --cov=music_theory.compose --cov-branch --cov-fail-under=100 +``` +exits 0. It covers: a 3/4 and a 6/8 score render bar-exact loops, two named +voices stack, the noise-drum voice is deterministic for a fixed seed, and an +A/B-section score renders A then B in order. + +## verify +Desktop. Render one 3/4 and one 6/8 example score with +`PYTHONPATH=. python -m music_theory render --score examples/waltz.txt --out /tmp/waltz.wav`, +check `analysis.dominant_frequency` and the frame count equal +`sequencer.loop_frames` for the bar count, and re-render for byte-identical +output. The final listen is kuhy's. + +## read first +- `music_theory/compose.py` (`Recipe`), `music_theory/sequencer.py` + (`NoteEvent`, `BEATS_PER_BAR`), `music_theory/noise.py` (drum source). +- `DOCS-article-walkthrough.md` (article section 10, notation as format). +- `~/.claude/skills/procedural-music/SKILL.md`. + +REMOVE ME AFTER FINISH diff --git a/unified-design-system/scripts/conformance.py b/unified-design-system/scripts/conformance.py new file mode 100644 index 0000000..b9a7168 --- /dev/null +++ b/unified-design-system/scripts/conformance.py @@ -0,0 +1,206 @@ +"""Design-token conformance per UI surface: tokens used vs raw literals. + +Install counts and component counts are vanity metrics. What shows whether a +design system is working is how often a surface reaches for a token versus a +hard-coded value (learn.thedesignsystem.guide, "Half my design system metrics +don't work anymore"). This script counts, per UI surface of one repo: + + * token usages: ``var(--x)`` in CSS/TS/TSX/Astro, ``AppPalette.x`` style + constants and ``Theme.of`` / ``colorScheme`` / ``textTheme`` in Dart + * raw literals: hex colours (``#rrggbb``, ``0xAARRGGBB``) and non-zero + ``px`` / ``pt`` numbers, outside the token files + +A surface is the nearest ancestor directory holding a manifest +(``pubspec.yaml``, ``package.json``, ``astro.config.*``), else the repo root. +Token files (tokens/theme/palette/colors), custom-property definitions, +comments, tests, ``@media`` lines and generated/build dirs are skipped. +The counts are a heuristic trend signal, not an audit. + +Run: python3 conformance.py [--json] [--previous ] +Exit 0 = report printed; 2 = bad arguments or unreadable previous file. +""" + +from __future__ import annotations + +import argparse +import json +import os +import re +import sys +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +from conformance_diff import delta, load_previous, table + +Json = dict[str, Any] + +EXTENSIONS = frozenset({".dart", ".css", ".ts", ".tsx", ".astro"}) +SKIP_DIRS = frozenset({ + ".git", "node_modules", "build", "dist", ".dart_tool", ".astro", ".next", + "coverage", "generated", "__pycache__", ".venv", "test", "tests", + "__tests__", ".fvm", ".pub-cache", +}) +SKIP_NAME = re.compile( + r"(\.g\.dart|\.freezed\.dart|\.d\.ts|\.test\.[jt]sx?|_test\.dart)$" +) +TOKEN_FILE = re.compile(r"^_?(tokens?|theme|palette|colou?rs?)\b", re.IGNORECASE) +MANIFESTS = ("pubspec.yaml", "package.json") +MAX_BYTES = 1_000_000 + +BLOCK_COMMENT = re.compile(r"/\*.*?\*/", re.DOTALL) +LINE_COMMENT = re.compile(r"(? float | None: + """Percent of style values that are tokens, or None with no values.""" + total = self.token_usages + self.raw_literals + if total == 0: + return None + return round(100.0 * self.token_usages / total, 1) + + def as_dict(self) -> Json: + """JSON-ready form.""" + return { + "name": self.name, + "files": self.files, + "token_usages": self.token_usages, + "raw_literals": self.raw_literals, + "conformance": self.conformance, + } + + +def strip_comments(text: str, suffix: str) -> str: + """Remove comments so a hex in prose is not counted as a literal.""" + text = BLOCK_COMMENT.sub("", text) + if suffix == ".css": + return text + return LINE_COMMENT.sub("", text) + + +def count_text(text: str, suffix: str) -> tuple[int, int]: + """Return (token usages, raw literals) for one file's text.""" + tokens = 0 + literals = 0 + for line in strip_comments(text, suffix).splitlines(): + tokens += len(TOKEN_USE.findall(line)) + if MEDIA_LINE.search(line): + continue + line = TOKEN_DEFINITION.sub("", line) + literals += len(HEX.findall(line)) + len(UNIT.findall(line)) + return tokens, literals + + +def _has_manifest(directory: Path) -> bool: + """True when ``directory`` holds a package/app manifest.""" + return any((directory / m).exists() for m in MANIFESTS) or any( + directory.glob("astro.config.*") + ) + + +def surface_root(path: Path, repo: Path) -> Path: + """Nearest ancestor of ``path`` (within ``repo``) holding a manifest.""" + parent = path.parent + while parent != repo and not _has_manifest(parent): + parent = parent.parent + return parent + + +def ui_files(repo: Path) -> list[Path]: + """UI source files, skipping build/generated dirs and token files.""" + found: list[Path] = [] + for root, dirs, names in os.walk(repo): + dirs[:] = sorted(d for d in dirs if d not in SKIP_DIRS) + for name in sorted(names): + path = Path(root) / name + if path.suffix not in EXTENSIONS or SKIP_NAME.search(name): + continue + if TOKEN_FILE.match(path.stem): + continue + if path.is_file() and path.stat().st_size <= MAX_BYTES: + found.append(path) + return found + + +def scan(repo: Path) -> list[Surface]: + """Count every UI surface under ``repo``, sorted by name.""" + surfaces: dict[Path, Surface] = {} + for path in ui_files(repo): + root = surface_root(path, repo) + name = str(root.relative_to(repo)) + surface = surfaces.setdefault(root, Surface(name)) + text = path.read_text(encoding="utf-8", errors="replace") + tokens, literals = count_text(text, path.suffix) + surface.files += 1 + surface.token_usages += tokens + surface.raw_literals += literals + return sorted(surfaces.values(), key=lambda s: s.name) + + +def totals(surfaces: list[Surface]) -> Surface: + """Sum of all surfaces.""" + return Surface( + "TOTAL", + sum(s.files for s in surfaces), + sum(s.token_usages for s in surfaces), + sum(s.raw_literals for s in surfaces), + ) + + +def report(repo: Path) -> Json: + """Whole-repo report as a JSON-ready dict.""" + surfaces = scan(repo) + return { + "repo": str(repo), + "surfaces": [s.as_dict() for s in surfaces], + "totals": totals(surfaces).as_dict(), + } + + +def main(argv: list[str] | None = None) -> int: + """CLI entry point.""" + parser = argparse.ArgumentParser(description=__doc__.split("\n")[0]) + parser.add_argument("repo", type=Path) + parser.add_argument("--json", action="store_true", help="print JSON") + parser.add_argument("--previous", type=Path, help="earlier --json output") + args = parser.parse_args(argv) + if not args.repo.is_dir(): + print(f"not a directory: {args.repo}", file=sys.stderr) + return 2 + rep = report(args.repo.resolve()) + diff = None + if args.previous: + try: + diff = delta(rep, load_previous(args.previous)) + except (OSError, ValueError) as exc: + print(f"cannot read previous report: {exc}", file=sys.stderr) + return 2 + if args.json: + print(json.dumps({**rep, **({"delta": diff} if diff else {})}, indent=2)) + else: + print(table(rep, diff)) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/unified-design-system/scripts/conformance_diff.py b/unified-design-system/scripts/conformance_diff.py new file mode 100644 index 0000000..647ffa3 --- /dev/null +++ b/unified-design-system/scripts/conformance_diff.py @@ -0,0 +1,74 @@ +"""Delta and table rendering for conformance reports (see conformance.py).""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +Json = dict[str, Any] + + +def _num(value: object) -> float: + """Numeric value from loosely typed JSON, 0 when absent.""" + return float(value) if isinstance(value, (int, float)) else 0.0 + + +def load_previous(path: Path) -> Json: + """Read an earlier ``--json`` report; ValueError if its shape is wrong.""" + data = json.loads(path.read_text(encoding="utf-8")) + surfaces = data.get("surfaces") if isinstance(data, dict) else None + ok = isinstance(surfaces, list) and isinstance(data.get("totals"), dict) + if not ok or not all(isinstance(s, dict) and "name" in s for s in surfaces): + msg = "not a conformance report" + raise ValueError(msg) + return data + + +def delta(current: Json, previous: Json) -> Json: + """Per-surface and total change versus a previous report.""" + before = {str(s["name"]): s for s in previous["surfaces"]} + rows: list[Json] = [] + entries = [*current["surfaces"], current["totals"]] + prev_total = previous["totals"] + for row in entries: + old = prev_total if row["name"] == "TOTAL" else before.get(str(row["name"]), {}) + rows.append({ + "name": row["name"], + "token_usages": _num(row["token_usages"]) - _num(old.get("token_usages")), + "raw_literals": _num(row["raw_literals"]) - _num(old.get("raw_literals")), + "conformance": None + if row["conformance"] is None or old.get("conformance") is None + else round(_num(row["conformance"]) - _num(old["conformance"]), 1), + }) + return {"rows": rows} + + +def _fmt(value: object, signed: bool = False) -> str: + """Table cell text for a count, percent or missing value.""" + if value is None: + return "-" + return f"{_num(value):+g}" if signed else f"{_num(value):g}" + + +def table(rep: Json, diff: Json | None) -> str: + """Plain-text table, with delta columns when ``diff`` is given.""" + head = ["surface", "files", "tokens", "raw", "conf%"] + if diff: + head += ["d.tokens", "d.raw", "d.conf"] + rows = [head] + deltas = {str(r["name"]): r for r in diff["rows"]} if diff else {} + for row in [*rep["surfaces"], rep["totals"]]: + cells = [str(row["name"]), _fmt(row["files"]), _fmt(row["token_usages"]), + _fmt(row["raw_literals"]), _fmt(row["conformance"])] + if diff: + d = deltas.get(str(row["name"]), {}) + cells += [_fmt(d.get("token_usages", 0), True), + _fmt(d.get("raw_literals", 0), True), + _fmt(d.get("conformance"), True)] + rows.append(cells) + widths = [max(len(r[i]) for r in rows) for i in range(len(head))] + return "\n".join( + " ".join(c.ljust(w) for c, w in zip(r, widths, strict=True)).rstrip() + for r in rows + ) diff --git a/unified-design-system/tests/conftest.py b/unified-design-system/tests/conftest.py new file mode 100644 index 0000000..33c2b9d --- /dev/null +++ b/unified-design-system/tests/conftest.py @@ -0,0 +1,6 @@ +"""Make ``scripts/`` importable: the folder name has a hyphen, so no package.""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts")) diff --git a/unified-design-system/tests/test_conformance.py b/unified-design-system/tests/test_conformance.py new file mode 100644 index 0000000..844c405 --- /dev/null +++ b/unified-design-system/tests/test_conformance.py @@ -0,0 +1,118 @@ +"""Tests for scripts/conformance.py (100 % branch coverage).""" + +from __future__ import annotations + +import json +import runpy +import sys +from typing import TYPE_CHECKING + +import conformance +import pytest + +if TYPE_CHECKING: + from pathlib import Path + + +def write(root: Path, rel: str, text: str) -> None: + path = root / rel + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(text, encoding="utf-8") + + +def test_count_text_css_tokens_literals_and_skips() -> None: + css = ( + "/* #123456 in a comment */\n" + ":root { --brand: #b8862e; }\n" + "@media (min-width: 600px) { a { color: red; } }\n" + ".a { color: var(--brand); margin: 12px 0px; border: 1.5pt solid #fff; }\n" + ) + assert conformance.count_text(css, ".css") == (1, 3) + + +def test_count_text_line_comments_and_urls_in_ts() -> None: + ts = 'const u = "http://x.test"; // #abcdef\nconst c = "#a1b2c3";\n' + assert conformance.count_text(ts, ".ts") == (0, 1) + + +def test_count_text_dart_tokens_and_literals() -> None: + dart = ( + "final c = Color(0xFF112233);\n" + "final d = AppPalette.accent;\n" + "final e = Theme.of(context).colorScheme.primary;\n" + ) + assert conformance.count_text(dart, ".dart") == (3, 1) + + +def test_conformance_percent_and_none() -> None: + assert conformance.Surface("x").conformance is None + assert conformance.Surface("x", 1, 3, 1).conformance == 75.0 + + +def test_scan_groups_by_manifest_and_skips_generated(tmp_path: Path) -> None: + write(tmp_path, "pubspec.yaml", "name: a") + write(tmp_path, "lib/ui.dart", "final c = AppPalette.bg; final d = '#fff';") + write(tmp_path, "lib/tokens.dart", "const c = Color(0xFF000000);") + write(tmp_path, "lib/a.g.dart", "const c = '#ffffff';") + write(tmp_path, "build/out.css", "a { color: #fff }") + write(tmp_path, "test/x_test.dart", "const c = '#fff';") + write(tmp_path, "web/package.json", "{}") + write(tmp_path, "web/s.css", "a { margin: 8px; color: var(--x); }") + write(tmp_path, "site/astro.config.mjs", "export default {}") + write(tmp_path, "site/p.astro", "

hi

") + write(tmp_path, "loose/readme.txt", "#fff 3px") + write(tmp_path, "root.tsx", "const a = 'var(--r)';") + write(tmp_path, "big.css", "a{}" + " " * conformance.MAX_BYTES) + surfaces = {s.name: s for s in conformance.scan(tmp_path)} + assert sorted(surfaces) == [".", "site", "web"] + assert (surfaces["."].token_usages, surfaces["."].raw_literals) == (2, 1) + assert surfaces["."].files == 2 + assert (surfaces["web"].token_usages, surfaces["web"].raw_literals) == (1, 1) + assert (surfaces["site"].token_usages, surfaces["site"].raw_literals) == (0, 1) + + +def test_scan_ignores_non_regular_file(tmp_path: Path) -> None: + (tmp_path / "dangling.css").symlink_to(tmp_path / "missing.css") + assert conformance.scan(tmp_path) == [] + + +def test_totals_and_report(tmp_path: Path) -> None: + write(tmp_path, "a.css", "a { margin: 8px; }") + write(tmp_path, "web/package.json", "{}") + write(tmp_path, "web/b.css", "b { color: var(--x); }") + rep = conformance.report(tmp_path) + assert rep["totals"]["token_usages"] == 1 + assert rep["totals"]["raw_literals"] == 1 + assert rep["totals"]["conformance"] == 50.0 + assert [s["name"] for s in rep["surfaces"]] == [".", "web"] + + +def test_main_table_json_and_delta(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None: + repo = tmp_path / "repo" + write(repo, "a.css", "a { margin: 8px; color: var(--x); }") + assert conformance.main([str(repo)]) == 0 + assert "TOTAL" in capsys.readouterr().out + assert conformance.main([str(repo), "--json"]) == 0 + first = capsys.readouterr().out + assert "delta" not in json.loads(first) + prev = tmp_path / "prev.json" + prev.write_text(first, encoding="utf-8") + write(repo, "b.css", "b { color: var(--y); }") + assert conformance.main([str(repo), "--json", "--previous", str(prev)]) == 0 + assert json.loads(capsys.readouterr().out)["delta"]["rows"][-1]["token_usages"] == 1.0 + assert conformance.main([str(repo), "--previous", str(prev)]) == 0 + assert "d.tokens" in capsys.readouterr().out + + +def test_main_errors(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None: + assert conformance.main([str(tmp_path / "nope")]) == 2 + assert "not a directory" in capsys.readouterr().err + assert conformance.main([str(tmp_path), "--previous", str(tmp_path / "none.json")]) == 2 + assert "cannot read previous" in capsys.readouterr().err + + +def test_script_entry_point(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(sys, "argv", ["conformance.py", str(tmp_path)]) + with pytest.raises(SystemExit) as exit_info: + runpy.run_path(conformance.__file__, run_name="__main__") + assert exit_info.value.code == 0 diff --git a/unified-design-system/tests/test_conformance_diff.py b/unified-design-system/tests/test_conformance_diff.py new file mode 100644 index 0000000..dfb3ab7 --- /dev/null +++ b/unified-design-system/tests/test_conformance_diff.py @@ -0,0 +1,71 @@ +"""Tests for scripts/conformance_diff.py (100 % branch coverage).""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import conformance_diff +import pytest + +if TYPE_CHECKING: + from pathlib import Path + + +def previous_report(conf: float | None = 50.0) -> dict: + return { + "surfaces": [{"name": ".", "token_usages": 1, "raw_literals": 1, "conformance": conf}], + "totals": {"name": "TOTAL", "token_usages": 1, "raw_literals": 1, "conformance": conf}, + } + + +def current_report() -> dict: + return { + "surfaces": [ + {"name": ".", "files": 1, "token_usages": 3, "raw_literals": 1, "conformance": 75.0}, + {"name": "new", "files": 1, "token_usages": 2, "raw_literals": 0, "conformance": None}, + ], + "totals": {"name": "TOTAL", "files": 2, "token_usages": 5, "raw_literals": 1, "conformance": 83.3}, + } + + +def test_delta_rows() -> None: + rows = {r["name"]: r for r in conformance_diff.delta(current_report(), previous_report())["rows"]} + assert rows["."] == {"name": ".", "token_usages": 2.0, "raw_literals": 0.0, "conformance": 25.0} + assert rows["new"]["token_usages"] == 2.0 + assert rows["new"]["conformance"] is None + assert rows["TOTAL"]["conformance"] == 33.3 + + +def test_delta_missing_previous_conformance() -> None: + rows = conformance_diff.delta(current_report(), previous_report(None))["rows"] + assert rows[0]["conformance"] is None + + +def test_num_and_fmt() -> None: + assert conformance_diff._num("x") == 0.0 + assert conformance_diff._num(2) == 2.0 + assert conformance_diff._fmt(None) == "-" + assert conformance_diff._fmt(2.5) == "2.5" + assert conformance_diff._fmt(2, signed=True) == "+2" + + +def test_table_without_and_with_delta() -> None: + plain = conformance_diff.table(current_report(), None) + assert plain.splitlines()[0].split() == ["surface", "files", "tokens", "raw", "conf%"] + assert "TOTAL" in plain + diff = conformance_diff.delta(current_report(), previous_report()) + text = conformance_diff.table(current_report(), diff) + assert "d.conf" in text + assert "+25" in text + + +@pytest.mark.parametrize( + "payload", + ["[]", "{}", '{"surfaces": 1, "totals": {}}', '{"surfaces": [1], "totals": {}}', + '{"surfaces": [], "totals": 1}'], +) +def test_load_previous_rejects_bad_shape(tmp_path: Path, payload: str) -> None: + path = tmp_path / "p.json" + path.write_text(payload, encoding="utf-8") + with pytest.raises(ValueError, match="not a conformance report"): + conformance_diff.load_previous(path)