feat(design-systems): the master sheet — purpose tokens, not per-element values
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 7s
CI & Build / integration (push) Successful in 16s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 42s
CI & Build / Build & push image (push) Successful in 34s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 7s
CI & Build / integration (push) Successful in 16s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 42s
CI & Build / Build & push image (push) Successful in 34s
Operator's new requirement (#2299, architecture in #2296): a design system does not just hold tokens, it generates and manages a master CSS sheet. That settles the milestone's open "authority mechanism" question — the record is authoritative because the stylesheet comes out of it. **The sheet is shaped by purpose and styles no elements.** It declares custom properties, grouped by what they mean, and contains no `.btn-primary`, no `table`, no `input`. That is the design, not a shortcut: a sheet that styled elements would restate the same handful of values once per element and grow with the UI, where purpose-named values are stated once and reused. Components live as SNIPPETS that reference these names — a surface that already exists and already carries prose, locations, drift checks, merge and write-path recall. A token named after an element (`--fs-button-bg`) is the smell that the two have been mixed; a purpose name (`--fs-action-primary`) is reused across all of them. Alongside the CSS the endpoint returns what the text cannot say for itself: which tokens are still valueless, and which VALUES are declared under more than one name. The second is the operator's "reuse consistent values" constraint made checkable — and it reports rather than refuses, because a design system legitimately aligns colours on purpose ("Success = Moss, by design") and only a human knows which case it is. Mode maps to selector the way the codebase already does it: base on the root selector, every other mode layered on `[data-theme="…"]`. The root selector is a PARAMETER — #251 recorded that a container-scoped preview cannot use `:root`, so hardcoding it would have made the generator useless to the preview surface. A token the rulebook names but states no value for is emitted as a commented-out declaration IN ITS GROUP rather than dropped. Its absence is the finding, and a comment puts that finding where the reader already is. Values are validated, not escaped, and this is a real boundary rather than tidiness: design systems are shareable records (rule #47), so `red; } body { display: none` in a system shared with you would otherwise inject CSS into your page. A value containing `{ } ; @ < >`, a comment delimiter or a newline is REFUSED and rendered as a comment saying so — rejecting beats stripping, since a partially-sanitised value is one the operator never wrote and the sheet's whole claim is that it is the record. Not in scope, and deliberately: serving this as the app's actual stylesheet. Generating and exposing a sheet is reversible; swapping theme.css for a generated one is not, and it should be an explicit call rather than a side effect.
This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
"""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 (
|
||||
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", {})]) == {}
|
||||
@@ -24,7 +24,8 @@ def test_route_handlers_callable():
|
||||
for name in (
|
||||
"list_design_systems", "create_design_system", "get_design_system",
|
||||
"update_design_system", "delete_design_system", "resolve_design_system",
|
||||
"import_design_system", "list_design_tokens", "create_design_token",
|
||||
"import_design_system", "get_design_system_stylesheet",
|
||||
"list_design_tokens", "create_design_token",
|
||||
"update_design_token", "delete_design_token", "set_project_design_system",
|
||||
):
|
||||
assert callable(getattr(routes, name))
|
||||
@@ -45,6 +46,7 @@ def test_every_endpoint_is_reachable_on_the_app():
|
||||
"/api/design-systems/<int:design_system_id>",
|
||||
"/api/design-systems/<int:design_system_id>/resolved",
|
||||
"/api/design-systems/<int:design_system_id>/import",
|
||||
"/api/design-systems/<int:design_system_id>/stylesheet",
|
||||
"/api/design-systems/<int:design_system_id>/tokens",
|
||||
"/api/design-tokens/<int:token_id>",
|
||||
"/api/projects/<int:project_id>/design-system",
|
||||
@@ -59,6 +61,7 @@ def test_service_functions_take_user_id():
|
||||
"update_design_system", "delete_design_system", "resolve_design_system",
|
||||
"create_token", "list_tokens", "update_token", "delete_token",
|
||||
"set_project_design_system", "import_from_rulebook",
|
||||
"stylesheet_for_system",
|
||||
):
|
||||
fn = getattr(svc, fn_name)
|
||||
assert callable(fn)
|
||||
@@ -81,6 +84,7 @@ def test_agent_and_web_surfaces_stay_at_parity():
|
||||
"resolve_design_system", "update_design_system", "delete_design_system",
|
||||
"create_design_token", "list_design_tokens", "update_design_token",
|
||||
"delete_design_token", "set_project_design_system",
|
||||
"get_design_system_stylesheet",
|
||||
):
|
||||
assert callable(getattr(tools, name)), f"MCP tool missing: {name}"
|
||||
assert callable(getattr(routes, name)), f"REST route missing: {name}"
|
||||
|
||||
@@ -373,3 +373,49 @@ async def test_an_unreadable_or_empty_rulebook_reports_nothing_rather_than_faili
|
||||
from scribe.services.design_systems import import_from_rulebook
|
||||
report = await import_from_rulebook(1, 3, 9, apply=True)
|
||||
assert report == {"rulebook_id": 9, "proposed": [], "created": [], "skipped": []}
|
||||
|
||||
|
||||
# --- the master sheet -------------------------------------------------------
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stylesheet_denied_returns_none():
|
||||
with patch("scribe.services.design_systems.resolve_design_system",
|
||||
AsyncMock(return_value=None)):
|
||||
from scribe.services.design_systems import stylesheet_for_system
|
||||
assert await stylesheet_for_system(1, 3) is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stylesheet_reports_what_the_sheet_cannot_say_for_itself():
|
||||
"""The CSS alone hides two things a reviewer needs: which tokens are still
|
||||
valueless, and which values are declared twice. Both ride alongside rather
|
||||
than being left for the reader to derive from the text."""
|
||||
from scribe.services.design_cascade import Contribution, ResolvedToken
|
||||
|
||||
def _resolved(name, base=None):
|
||||
return ResolvedToken(
|
||||
name=name,
|
||||
contributions=(
|
||||
{"base": (Contribution(system_id=1, value=base),)} if base else {}
|
||||
),
|
||||
group_name=None, purpose=None, supersedes=(), order_index=0,
|
||||
)
|
||||
|
||||
system = MagicMock()
|
||||
system.title = "FabledSword"
|
||||
with patch("scribe.services.design_systems.resolve_design_system",
|
||||
AsyncMock(return_value=[
|
||||
_resolved("--fs-moss", "#4a5d3f"),
|
||||
_resolved("--fs-success", "#4a5d3f"),
|
||||
_resolved("--fs-radius-sm"),
|
||||
])), \
|
||||
patch("scribe.services.design_systems.get_design_system",
|
||||
AsyncMock(return_value=system)):
|
||||
from scribe.services.design_systems import stylesheet_for_system
|
||||
result = await stylesheet_for_system(1, 3)
|
||||
|
||||
assert result["token_count"] == 3
|
||||
assert result["valueless"] == ["--fs-radius-sm"]
|
||||
assert result["duplicates"] == {"#4a5d3f": ["--fs-moss", "--fs-success"]}
|
||||
assert "--fs-moss: #4a5d3f;" in result["css"]
|
||||
assert "FabledSword" in result["css"]
|
||||
|
||||
Reference in New Issue
Block a user