feat(design-systems): check the components against the sheet they claim to use
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Successful in 16s
CI & Build / TypeScript typecheck (push) Successful in 32s
CI & Build / Python tests (push) Successful in 43s
CI & Build / Build & push image (push) Successful in 35s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Successful in 16s
CI & Build / TypeScript typecheck (push) Successful in 32s
CI & Build / Python tests (push) Successful in 43s
CI & Build / Build & push image (push) Successful in 35s
"The snippets use the tags from the sheet" was a relation nobody could verify.
Now it is three checks, and all three currently fail SILENTLY in this codebase:
unknown `var(--x)` where the system declares no `--x`. Renders as
nothing at all — no error, no failing test, no visual clue
beyond the element quietly not being styled.
superseded literals a value the sheet said to stop writing, paired with the
token to write instead. Only possible because `supersedes`
is declared rather than inferred.
local definitions custom properties a snippet mints for itself instead of
reusing the sheet's — the bloat a shared sheet exists to
prevent, where a value stops being reused and starts being
restated per component.
The first is not hypothetical. Writing DesignSystemsView.vue earlier in this
same session I used `--color-accent` throughout; it does not exist, and nothing
in the toolchain noticed. This check is the thing that would have.
A token that is both defined and read locally is reported ONCE, as an unknown
reference — "--btn-bg does not exist in the sheet" is the more precise statement
of the same problem, and reporting both would double-count one fact.
Literal matching is boundary-aware and case-insensitive: `#fff` must not fire
inside `#ffffff` (different colours, and a finding on the wrong one sends
someone to change correct code), while `#FFFFFF` in a rulebook has to match
`#ffffff` in a stylesheet — the same trap `normalize_hex` exists for.
Snippets with nothing to report are omitted entirely. A list of everything that
is fine is a list nobody reads twice — the same principle the auto-inject menu
and the drift panel are both built on.
Two integration mistakes fixed while wiring it: `list_snippets` returns
`(rows, total)` and caps its limit at 100, and `get_snippet` returns a Note
model rather than a dict. The list rows carry a preview, not the code, so the
check reads each full body — checking the preview would have reported on a
truncation.
This commit is contained in:
@@ -167,6 +167,41 @@ async def get_design_system_stylesheet(
|
||||
return result
|
||||
|
||||
|
||||
async def check_snippets_against_design_system(
|
||||
design_system_id: int,
|
||||
project_id: int = 0,
|
||||
) -> dict:
|
||||
"""Which recorded snippets disagree with a design system's sheet.
|
||||
|
||||
Snippets are the component layer — buttons, tables, input schemes — and they
|
||||
are supposed to use the tags the sheet declares. Three findings per snippet,
|
||||
each currently silent in the codebase:
|
||||
|
||||
unknown `var(--x)` where the system has no `--x`. Renders as
|
||||
NOTHING: no error, no failing test, just an element
|
||||
that quietly isn't styled.
|
||||
superseded_literals a literal the sheet says to stop writing, paired with
|
||||
the token to write instead.
|
||||
local_definitions custom properties the snippet mints for itself rather
|
||||
than using shared ones — the bloat a shared sheet
|
||||
exists to prevent.
|
||||
|
||||
Snippets with nothing to report are omitted. Reach for this before writing
|
||||
or reviewing component CSS.
|
||||
|
||||
Args:
|
||||
design_system_id: The system whose sheet is authoritative.
|
||||
project_id: Narrow to one project, or 0 for every project.
|
||||
"""
|
||||
uid = current_user_id()
|
||||
result = await ds_svc.check_snippets_against_system(
|
||||
uid, design_system_id, project_id
|
||||
)
|
||||
if result is None:
|
||||
raise ValueError(f"design system {design_system_id} not found")
|
||||
return result
|
||||
|
||||
|
||||
async def import_design_system_from_rulebook(
|
||||
design_system_id: int,
|
||||
rulebook_id: int,
|
||||
@@ -336,6 +371,7 @@ def register(mcp) -> None:
|
||||
update_design_system,
|
||||
delete_design_system,
|
||||
get_design_system_stylesheet,
|
||||
check_snippets_against_design_system,
|
||||
import_design_system_from_rulebook,
|
||||
create_design_token,
|
||||
list_design_tokens,
|
||||
|
||||
@@ -135,6 +135,24 @@ async def get_design_system_stylesheet(design_system_id: int):
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
@design_systems_bp.get("/design-systems/<int:design_system_id>/snippet-check")
|
||||
@login_required
|
||||
async def check_snippets_against_system(design_system_id: int):
|
||||
"""Which recorded snippets disagree with this system's sheet.
|
||||
|
||||
`?project_id=` narrows to one project; omit it to check every project, which
|
||||
is usually right — a component recorded elsewhere still has to use the same
|
||||
tags.
|
||||
"""
|
||||
project_id = request.args.get("project_id", type=int) or 0
|
||||
result = await ds_svc.check_snippets_against_system(
|
||||
_uid(), design_system_id, project_id
|
||||
)
|
||||
if result is None:
|
||||
return _not_found()
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
@design_systems_bp.post("/design-systems/<int:design_system_id>/import")
|
||||
@login_required
|
||||
async def import_design_system(design_system_id: int):
|
||||
|
||||
@@ -211,3 +211,84 @@ def duplicate_values(tokens: Sequence) -> dict[str, list[str]]:
|
||||
continue
|
||||
by_value.setdefault(str(base).strip().lower(), []).append(name)
|
||||
return {value: names for value, names in by_value.items() if len(names) > 1}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Reading a sheet from the other side: does this code use it correctly?
|
||||
# ---------------------------------------------------------------------------
|
||||
#
|
||||
# "The snippets use the tags from the sheet" is a verifiable relation, and
|
||||
# nothing checked it before. Three questions, each a different failure:
|
||||
#
|
||||
# var(--x) where no --x exists -> renders as NOTHING. No error, no test
|
||||
# failure, no visual clue beyond the thing
|
||||
# silently not being styled.
|
||||
# a superseded literal in code -> the value the sheet said to stop writing,
|
||||
# and the sheet knows what to write instead.
|
||||
# --x: declared inside a snippet -> a component minting its own token is the
|
||||
# bloat a shared sheet exists to prevent.
|
||||
#
|
||||
# The first is not hypothetical: `--color-accent` was used throughout a new view
|
||||
# in this codebase and does not exist.
|
||||
|
||||
_VAR_REFERENCE = re.compile(r"var\(\s*(--[A-Za-z0-9_-]+)")
|
||||
_LOCAL_DEFINITION = re.compile(r"(?<![\w-])(--[A-Za-z0-9_-]+)\s*:")
|
||||
|
||||
|
||||
def referenced_tokens(code: str) -> set[str]:
|
||||
"""Every custom property the code reads through `var()`."""
|
||||
return set(_VAR_REFERENCE.findall(code or ""))
|
||||
|
||||
|
||||
def defined_tokens(code: str) -> set[str]:
|
||||
"""Every custom property the code declares itself.
|
||||
|
||||
Excludes names it also reads: `--x: var(--x, fallback)` is a redeclaration
|
||||
of something the sheet owns, which the unknown-reference check already
|
||||
covers more precisely.
|
||||
"""
|
||||
return set(_LOCAL_DEFINITION.findall(code or "")) - referenced_tokens(code or "")
|
||||
|
||||
|
||||
def _literal_pattern(literal: str) -> re.Pattern:
|
||||
"""Match a literal value without matching a longer one that contains it.
|
||||
|
||||
`#fff` must not match inside `#ffffff` — they are different colours, and a
|
||||
finding that fired on the wrong one would send someone to change code that
|
||||
was already correct.
|
||||
"""
|
||||
escaped = re.escape(literal)
|
||||
lead = r"(?<![0-9A-Za-z_#-])"
|
||||
trail = r"(?![0-9A-Za-z_-])" if literal.startswith("#") else r"(?![0-9A-Za-z_-])"
|
||||
return re.compile(lead + escaped + trail, re.IGNORECASE)
|
||||
|
||||
|
||||
def check_code_against_tokens(code: str, tokens) -> dict:
|
||||
"""What this code gets wrong about that token set.
|
||||
|
||||
`tokens` is any sequence with `.name`, `.value_by_mode` and `.supersedes` —
|
||||
resolved tokens, or stored ones.
|
||||
|
||||
Reports rather than scores. Every finding here has a legitimate exception:
|
||||
a snippet may target a system it isn't being checked against, and a literal
|
||||
may be deliberate in a context the token doesn't cover. What it removes is
|
||||
the SILENCE — all three currently fail with no signal at all.
|
||||
"""
|
||||
known = {getattr(t, "name", "") for t in tokens}
|
||||
referenced = referenced_tokens(code)
|
||||
|
||||
superseded: list[dict] = []
|
||||
for token in tokens:
|
||||
for literal in getattr(token, "supersedes", None) or ():
|
||||
if _literal_pattern(str(literal)).search(code or ""):
|
||||
superseded.append({
|
||||
"literal": literal,
|
||||
"use_instead": getattr(token, "name", ""),
|
||||
})
|
||||
|
||||
return {
|
||||
"used": sorted(referenced & known),
|
||||
"unknown": sorted(referenced - known),
|
||||
"superseded_literals": superseded,
|
||||
"local_definitions": sorted(defined_tokens(code)),
|
||||
}
|
||||
|
||||
@@ -21,7 +21,11 @@ from scribe.models import async_session
|
||||
from scribe.models.design_system import DesignSystem, DesignToken
|
||||
from scribe.models.project import Project
|
||||
from scribe.services import access
|
||||
from scribe.services.design_stylesheet import duplicate_values, render_stylesheet
|
||||
from scribe.services.design_stylesheet import (
|
||||
check_code_against_tokens,
|
||||
duplicate_values,
|
||||
render_stylesheet,
|
||||
)
|
||||
from scribe.services.design_cascade import (
|
||||
ResolvedToken,
|
||||
ancestry,
|
||||
@@ -437,3 +441,57 @@ async def stylesheet_for_system(
|
||||
"valueless": [t.name for t in resolved if not t.value_by_mode],
|
||||
"duplicates": duplicate_values(resolved),
|
||||
}
|
||||
|
||||
|
||||
async def check_snippets_against_system(
|
||||
user_id: int, design_system_id: int, project_id: int = 0
|
||||
) -> dict | None:
|
||||
"""Which recorded snippets disagree with this design system's sheet.
|
||||
|
||||
The relation the operator named — "the snippets use the tags from the sheet"
|
||||
— turned into a check. For each snippet: `var(--x)` references with no such
|
||||
token, literals the sheet says to stop writing, and custom properties the
|
||||
snippet mints for itself instead of using shared ones.
|
||||
|
||||
Snippets with nothing to report are omitted entirely. A list of everything
|
||||
that is fine is a list nobody reads twice.
|
||||
"""
|
||||
resolved = await resolve_design_system(user_id, design_system_id)
|
||||
if resolved is None:
|
||||
return None
|
||||
|
||||
from scribe.services import snippets as snippets_svc
|
||||
|
||||
# `list_snippets` returns (rows, total) and caps limit at 100; `project_id`
|
||||
# must be None — not 0 — to reach across every project, since 0 would filter
|
||||
# to a project with that id.
|
||||
rows, _total = await snippets_svc.list_snippets(
|
||||
user_id=user_id, project_id=project_id or None, limit=100,
|
||||
)
|
||||
|
||||
findings: list[dict] = []
|
||||
for row in rows:
|
||||
snippet_id = row.get("id")
|
||||
if snippet_id is None:
|
||||
continue
|
||||
# The list rows carry a preview, not the code. The check has to read the
|
||||
# whole body or it would report on a truncation.
|
||||
note = await snippets_svc.get_snippet(user_id=user_id, snippet_id=int(snippet_id))
|
||||
if note is None:
|
||||
continue
|
||||
report = check_code_against_tokens(note.body or "", resolved)
|
||||
if not (
|
||||
report["unknown"] or report["superseded_literals"] or report["local_definitions"]
|
||||
):
|
||||
continue
|
||||
findings.append({
|
||||
"snippet_id": int(snippet_id),
|
||||
"title": note.title or "",
|
||||
**report,
|
||||
})
|
||||
|
||||
return {
|
||||
"design_system_id": design_system_id,
|
||||
"checked": len(rows),
|
||||
"findings": findings,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user