feat(design): the panel now asks whether the app agrees with its own sheet
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Successful in 19s
CI & Build / TypeScript typecheck (push) Successful in 34s
CI & Build / Python tests (push) Successful in 58s
CI & Build / Build & push image (push) Successful in 43s

Retiring rulebook #2 left the /design drift panel with no data source, and
because its empty state was well-written the feature read as working while it
could only ever render "nothing designated" (#2419). The original question is
genuinely gone: theme.css is generated from design system 2, so checking the
system against a sheet derived from it would be a tautology.

The question that survives is the one no server can answer. A generated sheet
still has to be LOADED and APPLIED, and nothing checked that it was:

  absent      the record declares a token the app doesn't have — the sheet was
              never regenerated after the record changed, or never loaded
  differs     the app has it with another value — a stale sheet, or a later
              rule that overrode it
  unrecorded  the app declares a token in the record's own family that the
              record has never heard of

Both sides go through the same engine so the comparison is honest: declared
values are set on an offscreen probe and read back, which performs the same
var() substitution the browser already did to the live values. Comparing raw
strings would mark every derived token as drift.

The designation moved with the feature — design_rulebook_id becomes
ui_design_system_id, with a migration deleting the retired key rather than
leaving an inert row. The prose extractor it fed goes too (#2288 said its
runtime role ended when the import landed).

Three orphans of the same shape, found alongside and fixed here:

- darkOverriddenNames hardcoded [data-theme="dark"]. The sheet went dark-first
  months ago, so it matched nothing and the "mode-aware" flag silently left the
  gallery. Now matches the SHAPE of a mode selector, which also holds for an
  install whose modes aren't light and dark.
- groupFor's prefix table never heard of --fs-, so 110 tokens sat under
  "other". Groups now come from the record where there is one; the table can
  only know families that shipped with the product (rule #115).
- The type scale was a hand-written table of nine sizes marked "no token",
  true when written and false since the scale was recorded. Now rendered from
  whatever size tokens the sheet declares, so it can't go stale twice.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs
This commit is contained in:
2026-08-03 20:50:03 -04:00
co-authored by Claude Opus 5
parent 841506b10c
commit 5b824c1626
12 changed files with 704 additions and 671 deletions
+31 -15
View File
@@ -1,29 +1,45 @@
"""Design-system surface — what the rulebook expects of the stylesheet.
"""This install's UI surface — which design system it claims to be built from.
The client owns the other half of the comparison: it reads live token values from
the browser (see `utils/designTokens.ts`), which is the only place they exist
resolved. This endpoint supplies the claims to check them against.
Kept separate from the design-systems CRUD blueprint on purpose. That one is
the RECORD: create a system, move a token, read the cascade. This one answers a
question about the RUNNING APP, and it exists because those are not the same
question. A design system can be a perfect record of a stylesheet the app never
loaded.
The client owns the other half. `utils/designTokens.ts` reads what the browser
actually resolved, which is the one thing no server can report, and compares it
to what this endpoint's system declares. So the comparison is
"does the app agree with its own sheet?" rather than "is the record
self-consistent?", which would be a tautology — the sheet is generated from the
record (#2419).
"""
from quart import Blueprint, jsonify
from scribe.auth import get_current_user_id, login_required
from scribe.services import design_rulebook_import as design_svc
from scribe.services import design_systems as ds_svc
design_bp = Blueprint("design", __name__, url_prefix="/api/design")
@design_bp.get("/expectations")
@design_bp.get("/ui-system")
@login_required
async def get_expectations():
"""Checkable claims from the rulebook this install designated as its design system.
async def get_ui_system():
"""The design system this install designated as the source of its own UI.
Returns `{"rulebook_id": int|null, "expectations": [...]}`.
Returns `{"design_system_id": int|null, "title": str|null}`.
`rulebook_id: null` is the NORMAL case, not an error — an install that has
not designated a design rulebook has nothing to compare against, and the
client shows an explanatory empty state (rule #115). Distinguishing it from
"designated but empty" is why the id is returned alongside the list.
Both nulls is the NORMAL case, not an error — an install that has not
designated one has nothing to check the running app against, and the client
shows an explanatory empty state (rule #115).
An id with a null title is the third case and the reason the id is returned
separately: designated, but deleted or not readable by this caller. Folding
that into "none designated" is precisely how a feature comes to render a
reassuring empty state forever.
"""
uid = get_current_user_id()
result = await design_svc.design_expectations(uid)
return jsonify(result.as_dict())
system_id, system = await ds_svc.ui_design_system(uid)
return jsonify({
"design_system_id": system_id,
"title": system.title if system else None,
})