utils/unified-design-system/scripts/structural_check.py
Krzysztof kuhy Rudnicki cbcee98d94 Add structural_check: spacing/radius/type/motion drift
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-11-02 08:18:27 +01:00

139 lines
4.9 KiB
Python

"""Verify spacing, radius, type, shadow and motion agree across all stacks.
`palette_check.py` guards colour only (every parser wants `#RRGGBB`), so these
scales were unprotected until now. Same two halves:
* agreement -- each mapped token resolves in every stack that is not `n/a`
for it, and all stacks carry the same value
* completeness -- every structural name any source defines is mapped or
exempted with a reason, in both directions (a stale exemption also fails)
Sources: DOCS-tokens.md + DOCS-motion.md (prose freeze), web_ui tokens.css,
design_system tokens.dart, gatelock _theme.py.
Run: python3 unified-design-system/scripts/structural_check.py
Exit 0 = agree and complete; 1 = drift, a missing value or an unmapped name.
"""
from __future__ import annotations
import re
import sys
from dataclasses import dataclass
from pathlib import Path
import structural_parse as sp
from structural_map import EXEMPT, STACKS, TOKENS
ROOT = Path(__file__).resolve().parents[2]
SOURCE_FILES: dict[str, str] = {
"md": "unified-design-system/DOCS-tokens.md",
"motion": "unified-design-system/DOCS-motion.md",
"css": "web_ui/src/tokens.css",
"dart": "design_system/lib/src/tokens.dart",
"tk": "gatelock/gatelock/_theme.py",
}
@dataclass
class Result:
"""One check outcome."""
ok: bool
detail: str
def norm(kind: str, raw: str) -> str:
"""Reduce a raw source value to a comparable string for its kind."""
if kind == "shadow":
return " ".join(raw.split())
nums = re.findall(r"-?\d*\.?\d+", raw)
if kind == "ease":
return ",".join(f"{float(n):g}" for n in nums)
if not nums:
return "0" if "zero" in raw else raw.strip()
return f"{float(nums[0]):g}"
def load(root: Path) -> dict[str, sp.Table]:
"""Parse every source under `root` into `{stack: {key: raw}}`."""
def read(key: str) -> str:
return (root / SOURCE_FILES[key]).read_text(encoding="utf-8")
return {
"md": {**sp.parse_tokens_md(read("md")), **sp.parse_motion_md(read("motion"))},
"css": sp.parse_css(read("css")),
"dart": sp.parse_dart(read("dart")),
"tk": sp.parse_tk(read("tk")),
}
def check_agreement(src: dict[str, sp.Table]) -> list[Result]:
"""Every mapped token must carry one value in every non-`n/a` stack."""
results: list[Result] = []
for token in TOKENS:
kind = token.canonical.split(".")[0]
names = {"md": token.canonical, **{s: getattr(token, s) for s in STACKS}}
seen: dict[str, str] = {}
missing: list[str] = []
for stack, name in names.items():
if name is None:
continue
if name in src[stack]:
seen[stack] = norm(kind, src[stack][name])
else:
missing.append(f"{stack}:{name} ({SOURCE_FILES[stack]})")
if missing:
results.append(Result(False, f"{token.canonical:<20} MISSING {', '.join(missing)}"))
elif len(set(seen.values())) > 1:
detail = ", ".join(f"{s}={v}" for s, v in seen.items())
files = ", ".join(SOURCE_FILES[s] for s in seen)
results.append(Result(False, f"{token.canonical:<20} DRIFT {detail} [{files}]"))
else:
results.append(
Result(True, f"{token.canonical:<20} {next(iter(seen.values()))} x{len(seen)}")
)
return results
def check_completeness(src: dict[str, sp.Table]) -> list[Result]:
"""No unmapped name in any source, and no exemption for a vanished name."""
results: list[Result] = []
for stack in ("md", *STACKS):
if stack == "md":
mapped = {t.canonical for t in TOKENS}
else:
mapped = {getattr(t, stack) for t in TOKENS if getattr(t, stack)}
exempt = EXEMPT.get(stack, {})
extras = sorted(n for n in src[stack] if n not in mapped and n not in exempt)
stale = sorted(n for n in exempt if n not in src[stack])
if extras:
msg = f"{stack:<4} defines unmapped name(s): {', '.join(extras)} -- add to TOKENS or EXEMPT"
results.append(Result(False, msg))
if stale:
results.append(Result(False, f"{stack:<4} stale exemption(s): {', '.join(stale)}"))
if not extras and not stale:
results.append(Result(True, f"{stack:<4} all {len(src[stack])} parsed name(s) accounted for"))
return results
def run(root: Path) -> list[Result]:
"""Both halves of the check against the sources under `root`."""
src = load(root)
return [*check_agreement(src), *check_completeness(src)]
def main(root: Path = ROOT) -> int:
"""Print the report; non-zero on any failing check."""
results = run(root)
for r in results:
print(("PASS " if r.ok else "FAIL ") + r.detail)
failures = sum(not r.ok for r in results)
print(f"\n{failures} failing check(s)")
return 1 if failures else 0
if __name__ == "__main__":
sys.exit(main())