#!/usr/bin/env python3 """Check the frontend's CSS against the tokens its stylesheet declares. The gap this closes: `services/design_stylesheet.check_code_against_tokens` has always been able to answer "does this code use the sheet correctly?", but the only thing ever fed to it was recorded SNIPPETS. The app's own components — where sixteen unresolvable references were found living quietly (#2319) — were checked by nothing at all. That was structural rather than an oversight. The drift panel runs in the browser and cannot read source files, and the server has no repo access. CI is the only place holding both the component sources and the ability to run the check, and it only became cheap once `theme.css` became a generated artifact — so the source of truth is a local file, with no network and no credentials. INSTANCE-AGNOSTIC ON PURPOSE (rule #115). Nothing here knows what a token should be called or which literals are discouraged. Both come from the stylesheet: the declarations, and the `SUPERSEDES` block the generator emits. Point it at a different install's sheet and it checks that install's rules. Two severities, and the split is deliberate: FAIL an unresolvable `var()` reference. Currently zero, so this is a ratchet that holds a line already reached rather than a backlog that keeps CI red. It also cannot false-positive: either the name is declared or it is not. REPORT superseded literals and raw colour literals. Hundreds today, so gating on them would mean a permanently failing job that everyone learns to ignore — which is worse than no check. """ from __future__ import annotations import argparse import pathlib import re import sys # A declaration is `--name:`; a reference is `var(--name)` or `var(--name, …)`. DECLARATION = re.compile(r"(?\s*(--[A-Za-z0-9_-]+)\s*$") HEX_LITERAL = re.compile(r"#[0-9a-fA-F]{3,8}\b") STYLE_BLOCK = re.compile(r"]*>(.*?)", re.S) CSS_COMMENT = re.compile(r"/\*.*?\*/", re.S) def declared_tokens(sheet: str) -> set[str]: """Every custom property the stylesheet declares. Anchored on the colon alone. Anchoring on `{` or `;` instead silently drops every declaration that follows a comment — a mistake made once already, which lost `--color-bg` and 2 others without erroring. """ return set(DECLARATION.findall(sheet)) def superseded_literals(sheet: str) -> dict[str, str]: """`{literal: token}` from the generator's SUPERSEDES block, lowercased.""" out: dict[str, str] = {} for line in sheet.splitlines(): match = SUPERSEDES_LINE.match(line) if match: out[match.group(1).lower()] = match.group(2) return out def _literal_pattern(literal: str) -> re.Pattern: """Match a literal without matching a longer one containing it. `#fff` must not fire inside `#ffffff`: different colours, and a finding on the wrong one sends someone to change correct code. """ return re.compile( r"(? str: """The CSS in a file — `