Files
FabledScribe/tests/test_design_stylesheet.py
T
bvandeusen 1fde646c60
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / TypeScript typecheck (push) Successful in 22s
CI & Build / integration (push) Successful in 30s
CI & Build / Python tests (push) Successful in 50s
CI & Build / Build & push image (push) Successful in 42s
feat(design-systems): formulas — derived tokens that follow their source
Operator: "build in a way to support formulas like this so that the colors shift
as expected and have less to clean up when testing color changes."

The storage needed no change at all, which is the good news. A formula is just a
value:

    --fs-accent-soft: color-mix(in srgb, var(--fs-accent) 15%, transparent)

It passes the value sanitiser untouched (verified, and now pinned by a test —
had `color-mix(... var(...) ...)` been rejected as unsafe, derivation would have
needed a storage shape of its own), and the browser resolves the `var()` at use
time. Change `--fs-accent` and everything derived from it shifts.

**One declaration covers every mode**, and that is the "less to clean up" part.
A derived token written once in the base layer follows its source through dark
mode automatically, because `var()` resolves where it is USED rather than where
it is written. A stored computed literal would need a row per mode and would
silently stop tracking the source the moment the source changed — the whole
problem this avoids.

What derivation DID need is the check. A formula pointing at a token that does
not exist is invalid-at-computed-value-time: the browser drops the declaration
outright and the token has no value. No error, no warning, nothing in the
toolchain notices — the same family as `--color-accent`, `_parent_map`, and the
scripted edit whose anchor matched nothing.

So `derivation_report` returns three things alongside the sheet: which tokens are
computed and from what, which formulas point at nothing, and which derive from
each other in a loop. CSS resolves a loop to nothing rather than hanging, so the
cycle check is about telling the operator, not protecting the renderer — but a
token that quietly resolves to nothing is exactly what is worth being told.

A self-reference with a fallback (`var(--fs-x, 8px)`) is deliberately not a
dependency; counting it would report every such token as a one-node loop.

The UI leads with broken formulas, then loops, then the healthy derived set —
the first two are unambiguously wrong, where a duplicate value is a judgement
call.
2026-07-31 09:51:54 -04:00

436 lines
17 KiB
Python

"""Rendering a design system as its master CSS sheet.
The generator is pure, so a whole sheet is one literal of tokens in and a string
out. Two things here are load-bearing rather than cosmetic: what the sheet
deliberately does NOT contain, and the fact that a value cannot escape its
declaration.
"""
from types import SimpleNamespace
from scribe.services.design_stylesheet import (
check_code_against_tokens,
derivation_report,
duplicate_values,
is_valid_token_name,
render_stylesheet,
safe_comment,
safe_value,
selector_for_mode,
)
def _token(name, value_by_mode, group_name=None, purpose=None):
return SimpleNamespace(
name=name, value_by_mode=value_by_mode,
group_name=group_name, purpose=purpose,
)
# --- safety -----------------------------------------------------------------
#
# Design systems are shareable records. A value that can close its declaration
# can inject arbitrary CSS into the page of anyone the system was shared with,
# which makes this a real boundary rather than tidiness.
def test_a_value_that_would_escape_its_declaration_is_refused():
"""`red; } body { display: none` is the whole attack: end the declaration,
close the block, open your own."""
assert safe_value("red; } body { display: none") is None
assert safe_value("#fff}") is None
assert safe_value("#fff;") is None
def test_at_rules_and_tags_are_refused():
assert safe_value("@import url(evil.css)") is None
assert safe_value("</style><script>") is None
def test_comment_delimiters_in_a_value_are_refused():
"""A value is not rendered inside a comment, but `/*` would comment out
every declaration after it — silently blanking the rest of the sheet."""
assert safe_value("red /* ") is None
assert safe_value("*/ red") is None
def test_a_newline_in_a_value_is_refused():
assert safe_value("red\n color: blue") is None
def test_ordinary_values_survive_untouched():
for value in ("#14171a", "8px", "cubic-bezier(0.2, 0.6, 0.2, 1)",
"0 4px 12px rgba(0,0,0,0.35)", "color-mix(in srgb, red 15%, transparent)"):
assert safe_value(value) == value
def test_a_rejected_value_is_dropped_not_cleaned_up():
"""Rejecting beats stripping. A partially-sanitised value is one the operator
never wrote, and the sheet's entire claim is that it IS the record — quietly
rendering a different colour would break that claim invisibly."""
css = render_stylesheet([_token("--fs-x", {"base": "red; } body { color: blue"})])
assert "body" not in css
assert "value rejected" in css
def test_comment_text_cannot_close_its_comment():
"""`purpose` is operator prose rendered into a comment — `*/` in it would
end the comment and spill the rest into the stylesheet as code."""
assert "*/" not in safe_comment("ends the comment */ then color: red")
def test_a_malformed_token_name_is_dropped():
"""A name is an identifier. A 'cleaned up' identifier is a different token
than the one recorded, so it is dropped rather than repaired."""
assert is_valid_token_name("--fs-obsidian")
assert not is_valid_token_name("--fs obsidian")
assert not is_valid_token_name("color: red")
assert not is_valid_token_name("fs-obsidian") # no leading --
css = render_stylesheet([_token("--bad name", {"base": "red"})])
assert "bad name" not in css
def test_a_mode_name_cannot_break_out_of_its_selector():
assert selector_for_mode('dark"] body {') == '[data-theme="darkbody"]'
# --- what the sheet is ------------------------------------------------------
def test_the_sheet_declares_properties_and_styles_no_elements():
"""THE shape decision. A sheet that styled elements would restate the same
handful of values once per element and grow with the UI. Purpose tokens are
stated once and reused; components are snippets that reference them."""
css = render_stylesheet([
_token("--fs-obsidian", {"base": "#14171a"}, group_name="surface"),
_token("--fs-moss", {"base": "#4a5d3f"}, group_name="action"),
])
assert "--fs-obsidian: #14171a;" in css
# No element or class rules — the sheet has exactly one block here, and
# every declaration in it is a custom property.
assert css.count("{") == 1
declarations = [
line.strip() for line in css.splitlines()
if ":" in line and line.strip().endswith(";")
]
assert declarations and all(d.startswith("--") for d in declarations)
def test_the_header_says_what_the_sheet_is_for():
"""A generated file with no explanation gets hand-edited, and then it has
diverged from the record it claims to be."""
css = render_stylesheet([_token("--fs-x", {"base": "1px"})], title="FabledSword")
assert "FabledSword" in css
assert "Generated" in css
assert "snippets" in css
# --- modes ------------------------------------------------------------------
def test_base_goes_on_the_root_selector_and_other_modes_layer_over_it():
"""Matches the convention already in the codebase, and the one-way scoping
#251 recorded: light on `:root`, dark layered on an attribute selector."""
css = render_stylesheet([
_token("--fs-bg", {"base": "#f5f1e8", "dark": "#14171a"}),
])
assert ":root {" in css
assert '[data-theme="dark"] {' in css
assert css.index(":root {") < css.index('[data-theme="dark"] {')
def test_a_mode_block_contains_only_what_that_mode_declares():
"""A mode block is an OVERRIDE layer, exactly as the storage model has it.
Repeating every token in every block would make the sheet claim each mode
redefines the whole system."""
css = render_stylesheet([
_token("--fs-bg", {"base": "#f5f1e8", "dark": "#14171a"}),
_token("--fs-radius-md", {"base": "8px"}),
])
dark_block = css.split('[data-theme="dark"] {')[1]
assert "--fs-bg" in dark_block
assert "--fs-radius-md" not in dark_block
def test_the_root_selector_is_caller_chosen():
"""A container-scoped preview cannot use `:root`. A generator that hardcoded
it could not serve the preview surface at all."""
css = render_stylesheet(
[_token("--fs-x", {"base": "1px"})], root_selector="[data-preview]"
)
assert "[data-preview] {" in css
assert ":root {" not in css
# --- grouping and honesty ---------------------------------------------------
def test_tokens_are_grouped_by_purpose_with_the_group_named():
css = render_stylesheet([
_token("--fs-obsidian", {"base": "#14171a"}, group_name="surface"),
_token("--fs-radius-md", {"base": "8px"}, group_name="radius"),
])
assert "/* surface */" in css
assert "/* radius */" in css
def test_a_purpose_becomes_an_inline_comment_on_the_base_layer_only():
"""Repeating the same prose in every mode block is noise: the token means
the same thing in dark mode."""
css = render_stylesheet([
_token("--fs-obsidian", {"base": "#14171a", "dark": "#000000"},
purpose="page bg, deepest surface"),
])
assert css.count("page bg, deepest surface") == 1
def test_a_declared_token_with_no_value_appears_as_a_comment_not_a_silence():
"""The rulebook named it, so its absence is a FINDING. A commented line puts
that finding where the reader is already looking; dropping it would make the
sheet look complete."""
css = render_stylesheet([
_token("--fs-obsidian", {"base": "#14171a"}),
_token("--fs-radius-sm", {}),
])
assert "--fs-radius-sm" in css
assert "no value set yet" in css
# Commented, so it cannot be mistaken for a live declaration.
assert " --fs-radius-sm:" not in css
def test_an_empty_system_renders_a_sheet_with_no_blocks_rather_than_failing():
"""A system with no tokens is an ordinary state (rule #115), including one
that was just created."""
css = render_stylesheet([])
assert "{" not in css
assert "Generated" in css
# --- the reuse report -------------------------------------------------------
def test_two_tokens_sharing_a_value_are_reported_not_refused():
"""The operator's constraint, made checkable: reuse consistent values rather
than restating them. But a design system legitimately aligns colours on
purpose — "Success = Moss, by design" — so this reports and lets a human
decide which it is."""
dupes = duplicate_values([
_token("--fs-moss", {"base": "#4A5D3F"}),
_token("--fs-success", {"base": "#4a5d3f"}),
_token("--fs-obsidian", {"base": "#14171a"}),
])
assert dupes == {"#4a5d3f": ["--fs-moss", "--fs-success"]}
def test_tokens_that_agree_in_one_mode_but_differ_in_another_are_not_duplicates():
"""A near-miss is a different, weaker finding, and calling it a duplicate
would send someone to merge two tokens that genuinely diverge."""
assert duplicate_values([
_token("--fs-a", {"base": "#fff", "dark": "#000"}),
_token("--fs-b", {"base": "#fff", "dark": "#111"}),
]) == {"#fff": ["--fs-a", "--fs-b"]}
def test_valueless_tokens_never_count_as_duplicates_of_each_other():
"""Otherwise every unfilled token would collide with every other one and the
report would be nothing but noise on a fresh import."""
assert duplicate_values([_token("--fs-a", {}), _token("--fs-b", {})]) == {}
# --- reading the sheet from the other side ----------------------------------
#
# "The snippets use the tags from the sheet" made checkable. All three findings
# below are currently SILENT in this codebase — no error, no failing test.
def _tok(name, base=None, supersedes=()):
return SimpleNamespace(
name=name,
value_by_mode={"base": base} if base else {},
supersedes=list(supersedes),
group_name=None, purpose=None,
)
SHEET = [
_tok("--fs-obsidian", "#14171a"),
_tok("--fs-parchment", "#e8e4d8", supersedes=["#fff", "#ffffff"]),
]
def test_a_reference_to_a_token_that_does_not_exist_is_reported():
"""THE bug this exists for. `var(--color-accent)` where no such token exists
renders as nothing at all — invisible focus rings, unstyled elements, no
error and no failing test. It happened in this codebase this session."""
report = check_code_against_tokens("outline: 2px solid var(--color-accent);", SHEET)
assert report["unknown"] == ["--color-accent"]
assert report["used"] == []
def test_a_reference_that_resolves_is_reported_as_used_not_as_a_finding():
report = check_code_against_tokens("background: var(--fs-obsidian);", SHEET)
assert report["used"] == ["--fs-obsidian"]
assert report["unknown"] == []
def test_a_superseded_literal_is_found_and_names_its_replacement():
"""The reframe paying off end to end: the finding says what to WRITE, not
merely what is wrong. That is only possible because the token declared it."""
report = check_code_against_tokens("color: #fff;", SHEET)
assert report["superseded_literals"] == [
{"literal": "#fff", "use_instead": "--fs-parchment"}
]
def test_a_shorter_hex_does_not_match_inside_a_longer_one():
"""`#fff` must not fire on `#ffffff` — different colours, and a finding on
the wrong one sends someone to change code that was already correct."""
report = check_code_against_tokens("color: #ffffffee;", SHEET)
assert [f["literal"] for f in report["superseded_literals"]] == []
def test_the_literal_match_is_case_insensitive():
"""Rulebooks write `#FFFFFF` and code writes `#ffffff`. A case-sensitive
check would silently find nothing — the same trap normalize_hex exists for."""
report = check_code_against_tokens("color: #FFFFFF;", SHEET)
assert report["superseded_literals"] == [
{"literal": "#ffffff", "use_instead": "--fs-parchment"}
]
def test_a_token_defined_and_read_locally_is_reported_once_not_twice():
"""A snippet minting its own custom property is the bloat a shared sheet
exists to prevent — but when it also reads it back, that is ONE fact. The
unknown-reference check owns it, because "--btn-bg does not exist in the
sheet" is the more precise statement of the same problem."""
report = check_code_against_tokens(".btn { --btn-bg: #333; background: var(--btn-bg); }", SHEET)
assert report["unknown"] == ["--btn-bg"]
assert report["local_definitions"] == []
def test_a_local_definition_never_read_back_is_still_reported():
report = check_code_against_tokens(".btn { --btn-bg: #333; }", SHEET)
assert report["local_definitions"] == ["--btn-bg"]
def test_a_declaration_that_is_not_a_custom_property_is_not_mistaken_for_one():
report = check_code_against_tokens(".btn { color: red; background: blue; }", SHEET)
assert report["local_definitions"] == []
def test_clean_code_reports_nothing_to_act_on():
report = check_code_against_tokens(
".btn { background: var(--fs-obsidian); color: var(--fs-parchment); }", SHEET
)
assert report["unknown"] == []
assert report["superseded_literals"] == []
assert report["local_definitions"] == []
assert report["used"] == ["--fs-obsidian", "--fs-parchment"]
def test_the_inline_comment_falls_back_to_rationale_when_there_is_no_purpose():
"""A token carrying only the why still says something in the sheet, rather
than rendering bare because the other field happened to be empty."""
token = SimpleNamespace(
name="--fs-success", value_by_mode={"base": "#4a5d3f"},
group_name=None, purpose=None, rationale="equals Moss, by design",
)
assert "equals Moss, by design" in render_stylesheet([token])
def test_purpose_wins_over_rationale_in_the_comment():
"""What a token is FOR is what a reader of the stylesheet needs first."""
token = SimpleNamespace(
name="--fs-x", value_by_mode={"base": "1px"},
group_name=None, purpose="hairline borders", rationale="because thin",
)
css = render_stylesheet([token])
assert "hairline borders" in css
assert "because thin" not in css
# --- derivation -------------------------------------------------------------
#
# A formula needs no special storage: `color-mix(..., var(--fs-accent) 15%, ...)`
# is a value, and the browser resolves it live. What it needs is a check, because
# a formula pointing at a missing token is dropped silently.
def _dtok(name, base):
return SimpleNamespace(
name=name, value_by_mode={"base": base},
group_name=None, purpose=None, supersedes=[],
)
ACCENT = "color-mix(in srgb, var(--fs-accent) 15%, transparent)"
def test_a_formula_survives_the_value_sanitiser_untouched():
"""LOAD-BEARING for the whole approach. If `color-mix(... var(...) ...)` were
rejected as unsafe, derivation would need a storage shape of its own."""
assert safe_value(ACCENT) == ACCENT
assert ACCENT in render_stylesheet([_dtok("--fs-accent-soft", ACCENT)])
def test_a_derived_token_reports_what_it_is_computed_from():
report = derivation_report([
_dtok("--fs-accent", "#5b4a8a"),
_dtok("--fs-accent-soft", ACCENT),
])
assert report["derived"] == {"--fs-accent-soft": ["--fs-accent"]}
assert report["unknown_refs"] == {}
def test_a_formula_pointing_at_a_token_that_does_not_exist_is_reported():
"""The browser drops the whole declaration — invalid at computed-value time —
and nothing errors. Exactly the failure mode this system exists to end."""
report = derivation_report([_dtok("--fs-accent-soft", ACCENT)])
assert report["unknown_refs"] == {"--fs-accent-soft": ["--fs-accent"]}
def test_a_plain_value_is_not_reported_as_derived():
report = derivation_report([_dtok("--fs-obsidian", "#14171a")])
assert report["derived"] == {}
def test_a_derivation_loop_is_reported_once():
"""CSS resolves a loop to nothing rather than hanging, so this is about
telling the operator — but a token that quietly resolves to nothing is
precisely the thing worth being told."""
report = derivation_report([
_dtok("--fs-a", "var(--fs-b)"),
_dtok("--fs-b", "var(--fs-a)"),
])
assert len(report["cycles"]) == 1
assert set(report["cycles"][0]) == {"--fs-a", "--fs-b"}
def test_a_chain_of_derivations_is_not_a_cycle():
"""a <- b <- c is ordinary and must not trip the loop check."""
report = derivation_report([
_dtok("--fs-a", "#000"),
_dtok("--fs-b", "var(--fs-a)"),
_dtok("--fs-c", "var(--fs-b)"),
])
assert report["cycles"] == []
assert report["derived"] == {"--fs-b": ["--fs-a"], "--fs-c": ["--fs-b"]}
def test_a_token_referencing_itself_is_not_treated_as_a_dependency():
"""`--fs-x: var(--fs-x, fallback)` is a self-reference with a fallback, not a
derivation — counting it would report every such token as a one-node loop."""
report = derivation_report([_dtok("--fs-x", "var(--fs-x, 8px)")])
assert report["cycles"] == []
assert report["derived"] == {}
def test_a_derived_token_needs_only_a_base_value_to_follow_every_mode():
"""The reason formulas beat computed literals. One declaration in the base
layer tracks its source through dark mode too, because `var()` resolves where
it is USED, not where it is written — so there is nothing to re-derive when a
colour changes."""
css = render_stylesheet([
SimpleNamespace(
name="--fs-accent", value_by_mode={"base": "#5b4a8a", "dark": "#7a68b0"},
group_name=None, purpose=None,
),
_dtok("--fs-accent-soft", ACCENT),
])
dark_block = css.split('[data-theme="dark"] {')[1]
assert "--fs-accent:" in dark_block
assert "--fs-accent-soft" not in dark_block # stated once, follows anyway