feat(design-systems): central prose — guidance on the system, rationale on tokens
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / integration (push) Successful in 19s
CI & Build / TypeScript typecheck (push) Failing after 20s
CI & Build / Python tests (push) Successful in 44s
CI & Build / Build & push image (push) Skipped

Last piece of the architecture in #2296. The operator: "the prose doesn't have
to live as one offs, there's a central system for managing it."

Two fields, both free-form:

  design_systems.guidance   the narrative a token table cannot hold — aesthetic,
                            voice and tone, what is deliberately out of scope.
  design_tokens.rationale   WHY a token is this value, which is a different
                            question from `purpose` (what it is FOR). "Success
                            equals Moss, aligned by design" is a rationale;
                            "page bg, deepest surface" is a purpose. Rules carry
                            the first routinely and a token row had nowhere to
                            put it.

Free-form rather than a column per category, deliberately. A schema with
`voice`, `aesthetic` and `scope` columns would bake one rulebook's table of
contents into every install (rule #115), leaving the next install three empty
columns and nowhere for what it actually cares about. Both nullable: a design
system with no prose at all is complete, not a draft.

`rationale` cascades like `purpose` — deepest non-empty wins — so an app
overriding a colour keeps the family's reasoning rather than blanking it. Same
argument as `supersedes`: the override was about the value, not the meaning.

In the generated sheet the inline comment prefers `purpose` and falls back to
`rationale`, so a token carrying only the why still says something instead of
rendering bare.
This commit is contained in:
2026-07-30 21:52:16 -04:00
parent 46d88f9e7e
commit 0f80b790c7
12 changed files with 203 additions and 11 deletions
+31
View File
@@ -387,3 +387,34 @@ def test_rows_without_a_supersedes_attribute_at_all_still_resolve():
)
resolved = _by_name(resolve_tokens(FAMILY, PARENTS, {FAMILY: [legacy]}))
assert resolved["--fs-x"].supersedes == ()
# --- rationale --------------------------------------------------------------
def test_rationale_cascades_like_purpose_and_is_a_different_question():
"""`purpose` is what the token is FOR; `rationale` is why it is this value.
Rules carry the second routinely ("Success = Moss, aligned by design") and a
token row had nowhere to put it until now."""
resolved = _by_name(resolve_tokens(
APP, PARENTS,
{
FAMILY: [SimpleNamespace(
name="--fs-success", value_by_mode={"base": "#4a5d3f"},
group_name="semantic", purpose="success states",
rationale="equals Moss, aligned by design",
order_index=0, supersedes=[],
)],
APP: [_token("--fs-success", {"base": "#3f5236"})],
},
))
token = resolved["--fs-success"]
assert token.rationale == "equals Moss, aligned by design"
assert token.purpose == "success states"
assert token.value_by_mode == {"base": "#3f5236"} # the value still overrode
def test_a_token_without_a_rationale_resolves_to_none():
resolved = _by_name(resolve_tokens(
FAMILY, PARENTS, {FAMILY: [_token("--fs-x", {"base": "1px"})]},
))
assert resolved["--fs-x"].rationale is None
+21
View File
@@ -319,3 +319,24 @@ def test_clean_code_reports_nothing_to_act_on():
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
+2 -1
View File
@@ -398,7 +398,8 @@ async def test_stylesheet_reports_what_the_sheet_cannot_say_for_itself():
contributions=(
{"base": (Contribution(system_id=1, value=base),)} if base else {}
),
group_name=None, purpose=None, supersedes=(), order_index=0,
group_name=None, purpose=None, rationale=None,
supersedes=(), order_index=0,
)
system = MagicMock()