feat(write-path): a UI write is told which design system binds it (#4256)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 11s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / integration (push) Successful in 1m4s
CI & Build / Python tests (push) Successful in 1m43s
CI & Build / Build & push image (push) Successful in 27s

A design system binds like a rule but reached a session only through the
session-start block: complete for a session that asks, silent for one
writing a component. The write-path hint now carries a design arm.

A trigger, not a search: a project has one design system, so the question
is answered by the file being UI (.vue, .css, .tsx, ...) in a project that
has one. No vectors, no score, no slot from the ranked menu.

An index, not the prose: resolved guidance runs to ~8,000 chars (the house
style alone), near the hook's whole additionalContext cap. The line names
each inherited layer's section headings and inlines a layer short enough to
be a line - in practice the leaf's departure.

The other arms do not move. A design-only write returns on its own rather
than joining the prior-art guard, so the standing-rule arm still runs only
where it ran before. Once per session per system, on the hook's existing
token-keyed channel (exclude_derive, design:<id>) - no plugin change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
This commit is contained in:
2026-09-21 23:02:44 -04:00
co-authored by Claude Opus 5
parent ad4bd101db
commit cef91f4cd4
2 changed files with 327 additions and 1 deletions
+116 -1
View File
@@ -2298,7 +2298,17 @@ async def build_write_path_hint(
# of this guard: that one runs a SEMANTIC search, and moving it here would
# run an embedding query on every write in the session. Its gating is a
# separate question from this one (see the note on #3244).
# The design arm (#4256) is decided HERE, above the guard, so a UI write
# that matched no prior art still carries it — and it returns on its own
# rather than joining the guard's condition, because joining it would let
# a design line switch the standing-rule arm below on for writes where it
# has never run, moving that arm's call distribution under its floor.
design_text, design_dedup = await _design_arm(
user_id, project_id, path, set(exclude_derive or []),
)
if not staleness and not synced and not menu and not stamped and not divergence and not derive:
if design_text:
return {**empty, "context": design_text, "derive_keys": [design_dedup]}
return empty
owners = await owner_names_for({
@@ -2320,6 +2330,9 @@ async def build_write_path_hint(
# Seeded with the staleness line, which is decided above the early
# return and so cannot wait for this list to exist.
lines: list[str] = list(staleness)
# First after staleness: it BINDS, where everything below is prior art.
if design_text:
lines.append(design_text)
sync_note_ids: list[int] = []
if synced:
# The sync framing (#2708). Deliberately imperative about the record —
@@ -2552,7 +2565,9 @@ async def build_write_path_hint(
"stamped": stamped,
"divergence": divergence,
"derive": derive,
"derive_keys": [d["key"] for d in derive],
"derive_keys": [d["key"] for d in derive] + (
[design_dedup] if design_dedup else []
),
"rule_ids": rule_ids,
"checkpoint": checkpoint,
}
@@ -2703,6 +2718,106 @@ async def build_tool_rule_hint(
return out
# --- the design-guidance arm (#4256) ----------------------------------------
# A design system BINDS like a rule, and until this it had one channel: the
# session-start block, which names it and the call that reads its prose. That
# is complete for a session that knows to ask and silent for one that is
# writing a component — the same gap every unasked arm exists to close.
#
# A TRIGGER, NOT A SEARCH. A project has exactly one design system
# (projects.design_system_id is a single FK), so there is nothing to rank and
# no vector to compute: the question "does this guidance apply here" is
# answered by the file being UI. Deterministic and cheap, and it takes no
# slot from the ranked menu — the band, floor and budget the other arms were
# tuned against are untouched by construction, which is why this adds no
# retrieval_logs row: there is no score distribution for it to join.
#
# AN INDEX, NOT THE PROSE. Resolved guidance runs to thousands of characters
# (a house style is long by nature), which would take most of the hook's
# additionalContext cap on its own. So the line names the SECTIONS of each
# inherited layer — the headings are self-describing ("Where the accent must
# NOT appear", "Voice and tone") the way rule titles are — and inlines only a
# layer short enough to be a line: in practice the leaf, since a child system
# holds just its departure from the house style. Choosing a paragraph by
# meaning would need the guidance embedded per section; that is justified
# only if this index turns out not to be read.
#
# ONCE PER SESSION PER SYSTEM, on the hook's token-keyed channel
# (`exclude_derive`, keyed `design:<id>`). That channel already dedups opaque
# keys on its own file, so the arm needs no new plugin state.
_DESIGN_UI_EXTENSIONS = frozenset({
".vue", ".svelte", ".css", ".scss", ".sass", ".less",
".tsx", ".jsx", ".html",
})
# A guidance layer this short is shown whole; anything longer is indexed.
_DESIGN_INLINE_CHARS = 500
_DESIGN_HEADING = re.compile(r"^##\s+(.+?)\s*$", re.M)
def design_key(design_system_id: int) -> str:
"""The dedup token for the design arm on the hook's keyed channel."""
return f"design:{int(design_system_id)}"
def is_ui_path(path: str) -> bool:
"""Whether writing `path` is writing UI — the design arm's trigger."""
name = (path or "").rsplit("/", 1)[-1].lower()
return any(name.endswith(ext) for ext in _DESIGN_UI_EXTENSIONS)
def _design_line(path: str, design: dict) -> str:
"""Name the design system that binds this file, and what its prose covers."""
ds_id = design["id"]
layers: list[str] = []
for layer in design.get("guidance") or []:
text = (layer.get("guidance") or "").strip()
if not text:
continue
flat = " ".join(text.split())
headings = _DESIGN_HEADING.findall(text)
if len(flat) <= _DESIGN_INLINE_CHARS:
layers.append(f"{layer['title']}: \"{flat}\"")
elif headings:
layers.append(f"{layer['title']} covers " + " · ".join(headings))
else:
short, _cut = elide(flat, _DESIGN_INLINE_CHARS)
layers.append(f"{layer['title']}: \"{short}\"")
inherits = (
" (inherits " + " ".join(design["inherits_from"]) + ")"
if design.get("inherits_from") else ""
)
out = (
f"> Design system binds `{path}`: {design['title']} (id {ds_id}){inherits}. "
f"Read `get_design_system({ds_id})` → `resolved_guidance` before writing "
f"UI here, and take values from `resolve_design_system({ds_id})` rather "
f"than hand-writing them."
)
if layers:
out += " " + "; ".join(layers) + "."
return out + " (Shown once per session.)"
async def _design_arm(
user_id: int, project_id: int, path: str, skip: set[str],
) -> tuple[str, str]:
"""(line, dedup key) for a UI write in a project with a design system,
or ("", "") — never raises: a design hint must never break a write."""
if not project_id or not is_ui_path(path):
return "", ""
try:
project = await projects_svc.get_project(user_id, project_id)
ds_id = getattr(project, "design_system_id", None) if project else None
if not ds_id or design_key(ds_id) in skip:
return "", ""
design = await design_systems_svc.design_context(user_id, ds_id)
if not design:
return "", ""
return _design_line(path, design), design_key(ds_id)
except Exception:
logger.debug("write-path design arm failed", exc_info=True)
return "", ""
def _derive_line(path: str, derive: list[dict]) -> str:
"""The ledger's word on the names being written (#2900): a duplicate
family to derive, or a canon to reuse — said at the write."""