feat(design-systems): formulas — derived tokens that follow their source
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

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.
This commit is contained in:
2026-07-31 09:51:54 -04:00
parent 23a385e2db
commit 1fde646c60
5 changed files with 242 additions and 0 deletions
+93
View File
@@ -9,6 +9,7 @@ from types import SimpleNamespace
from scribe.services.design_stylesheet import (
check_code_against_tokens,
derivation_report,
duplicate_values,
is_valid_token_name,
render_stylesheet,
@@ -340,3 +341,95 @@ def test_purpose_wins_over_rationale_in_the_comment():
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