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
+16
View File
@@ -27,6 +27,7 @@ from scribe.services.design_systems import DesignSystemCycle
async def create_design_system(
title: str,
description: str = "",
guidance: str = "",
parent_id: int = 0,
) -> dict:
"""Create a design system, optionally inheriting from another.
@@ -34,6 +35,8 @@ async def create_design_system(
Args:
title: What this system is, e.g. "FabledSword" or "Scribe" (required).
description: What it covers and when it applies.
guidance: The narrative a token table cannot hold — aesthetic, voice and
tone, what is deliberately out of scope. Markdown, free-form.
parent_id: Inherit from this system — it holds the defaults this one
overrides. Omit (0) for a top-level "family" system, which is what
a first design system usually is.
@@ -43,6 +46,7 @@ async def create_design_system(
uid,
title=title,
description=description or None,
guidance=guidance or None,
parent_id=parent_id or None,
)
if system is None:
@@ -98,6 +102,7 @@ async def update_design_system(
design_system_id: int,
title: str = "",
description: str = "",
guidance: str = "",
parent_id: int = 0,
) -> dict:
"""Update a design system.
@@ -106,6 +111,7 @@ async def update_design_system(
design_system_id: The system to update.
title: New title, or "" to leave unchanged.
description: New description, or "" to leave unchanged.
guidance: New guidance prose, or "" to leave unchanged.
parent_id: 0 = leave unchanged, -1 = clear (make this a top-level
family system), positive = inherit from that system. A parent that
already inherits from this system is refused — that would be a loop.
@@ -116,6 +122,8 @@ async def update_design_system(
fields["title"] = title
if description:
fields["description"] = description
if guidance:
fields["guidance"] = guidance
if parent_id:
fields["parent_id"] = None if parent_id == -1 else parent_id
try:
@@ -246,6 +254,7 @@ async def create_design_token(
value_by_mode: dict | None = None,
group_name: str = "",
purpose: str = "",
rationale: str = "",
supersedes: list | None = None,
order_index: int = 0,
) -> dict:
@@ -262,6 +271,9 @@ async def create_design_token(
group_name: Free-text grouping — "surface", "text", "radius", whatever
this system's own vocabulary is.
purpose: What the token is for, e.g. "page bg, deepest surface".
rationale: WHY it is this value — a different question from purpose.
"Success equals Moss, aligned by design" is a rationale; "page bg,
deepest surface" is a purpose.
supersedes: Literal values this token should be used INSTEAD OF, e.g.
["#fff", "#ffffff"]. This is how a design system records what a
prohibition was trying to say — not "white is banned" but "write
@@ -278,6 +290,7 @@ async def create_design_token(
value_by_mode=value_by_mode,
group_name=group_name or None,
purpose=purpose or None,
rationale=rationale or None,
supersedes=supersedes,
order_index=order_index,
)
@@ -299,6 +312,7 @@ async def update_design_token(
value_by_mode: dict | None = None,
group_name: str = "",
purpose: str = "",
rationale: str = "",
supersedes: list | None = None,
order_index: int = -1,
) -> dict:
@@ -318,6 +332,8 @@ async def update_design_token(
fields["group_name"] = group_name
if purpose:
fields["purpose"] = purpose
if rationale:
fields["rationale"] = rationale
# `is not None`, not truthiness: `[]` is a meaningful edit (drop every
# superseded literal) and would otherwise be unreachable.
if supersedes is not None: