feat(design-systems): give a design system a way to reach the session
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Successful in 28s
CI & Build / TypeScript typecheck (push) Successful in 37s
CI & Build / Python tests (push) Successful in 51s
CI & Build / Build & push image (push) Successful in 31s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Successful in 28s
CI & Build / TypeScript typecheck (push) Successful in 37s
CI & Build / Python tests (push) Successful in 51s
CI & Build / Build & push image (push) Successful in 31s
Storing a design system never made a session aware of one. Rules get pushed into every session by the SessionStart hook and returned by enter_project; a design system had neither, so its standards were reachable only by an agent that already knew to call resolve_design_system — the same silent failure as a token nobody declares. That gap was invisible while the operator's visual standards also lived in a rulebook. Retiring that rulebook (which is what this unblocks) would have deleted design guidance from every session with nothing to say so. - services/design_systems.design_context() — the delivery side. Guidance is chain-merged ANCESTOR-FIRST: a child system holds only what it CHANGES, so its own guidance describes a departure from a house style it never restates, and the leaf alone is a fragment. Tokens are summarised (count + group names), not listed — a hundred declarations would crowd out the context they are meant to inform. - enter_project returns `design_system`, null when the project has none. - The SessionStart context gains a Design system block with pointers to the values, alongside the always-on rules. - server.py's entity list gains Design system, including the negative: do NOT record one as a rulebook, because a token kept as prose cannot be resolved, inherited, rendered or checked. - The rulebook-tier passage used "a design-system rulebook" as its worked example of a subscribed rulebook — it now teaches the opposite, plus a new "is this a rule at all?" test pointing at design systems, processes and snippets. - using-scribe gains a section on building UI against the project's system. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs
This commit is contained in:
@@ -130,7 +130,11 @@ async def test_build_session_context_renders_titles_grouped_by_topic():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_session_context_includes_project_when_scoped():
|
||||
project = MagicMock(id=2, title="FabledScribe", goal="ship it")
|
||||
# design_system_id explicitly None: a bare MagicMock would hand back a truthy
|
||||
# auto-attribute and send this through the design branch, which is the
|
||||
# opposite of what this test is about.
|
||||
project = MagicMock(id=2, title="FabledScribe", goal="ship it",
|
||||
design_system_id=None)
|
||||
with patch("scribe.services.plugin_context.rulebooks_svc.list_always_on_rules",
|
||||
AsyncMock(return_value=[_rule(1, "rule", 1)])), \
|
||||
patch("scribe.services.plugin_context._topic_titles",
|
||||
@@ -145,6 +149,68 @@ async def test_build_session_context_includes_project_when_scoped():
|
||||
assert out["project"] == {"id": 2, "title": "FabledScribe"}
|
||||
assert "## Active project: FabledScribe (id 2)" in out["context"]
|
||||
assert "Open todo tasks: 4" in out["context"]
|
||||
# No design system on the project -> no design block at all. An install with
|
||||
# none is the ordinary case, not a degraded one.
|
||||
assert "## Design system" not in out["context"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_session_context_pushes_the_projects_design_system():
|
||||
"""The gap this closes: a design system had no push channel, so its
|
||||
standards reached a session only if the agent already knew to go looking —
|
||||
the same silent failure as a token nobody declares."""
|
||||
project = MagicMock(id=2, title="App", goal="", design_system_id=9)
|
||||
design = {
|
||||
"id": 9, "title": "App kit", "description": "",
|
||||
"inherits_from": ["House"],
|
||||
"guidance": [], "token_count": 95,
|
||||
"token_groups": ["accent", "surface", "type"],
|
||||
}
|
||||
with patch("scribe.services.plugin_context.rulebooks_svc.list_always_on_rules",
|
||||
AsyncMock(return_value=[_rule(1, "rule", 1)])), \
|
||||
patch("scribe.services.plugin_context._topic_titles",
|
||||
AsyncMock(return_value={1: "git-workflow"})), \
|
||||
patch("scribe.services.plugin_context.projects_svc.get_project",
|
||||
AsyncMock(return_value=project)), \
|
||||
patch("scribe.services.plugin_context.notes_svc.list_notes",
|
||||
AsyncMock(return_value=([], 0))), \
|
||||
patch("scribe.services.plugin_context.design_systems_svc.design_context",
|
||||
AsyncMock(return_value=design)):
|
||||
from scribe.services.plugin_context import build_session_context
|
||||
out = await build_session_context(user_id=7, project_id=2)
|
||||
|
||||
ctx = out["context"]
|
||||
assert "## Design system: App kit (id 9) (inherits House)" in ctx
|
||||
assert "95 tokens across accent, surface, type" in ctx
|
||||
# A pointer to the values, never the values themselves — a hundred token
|
||||
# declarations would crowd out the context they are meant to inform. The
|
||||
# summary carries counts and group NAMES only, which is why design_context
|
||||
# returns those rather than the resolved tokens.
|
||||
assert "resolve_design_system(9)" in ctx
|
||||
assert "get_design_system_stylesheet(9)" in ctx
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_session_context_survives_an_unreadable_design_system():
|
||||
"""design_context returns None when the caller may not read the system.
|
||||
That must degrade to "no design block", not to a crash that costs the
|
||||
session its rules too."""
|
||||
project = MagicMock(id=2, title="App", goal="", design_system_id=9)
|
||||
with patch("scribe.services.plugin_context.rulebooks_svc.list_always_on_rules",
|
||||
AsyncMock(return_value=[_rule(1, "rule", 1)])), \
|
||||
patch("scribe.services.plugin_context._topic_titles",
|
||||
AsyncMock(return_value={1: "git-workflow"})), \
|
||||
patch("scribe.services.plugin_context.projects_svc.get_project",
|
||||
AsyncMock(return_value=project)), \
|
||||
patch("scribe.services.plugin_context.notes_svc.list_notes",
|
||||
AsyncMock(return_value=([], 0))), \
|
||||
patch("scribe.services.plugin_context.design_systems_svc.design_context",
|
||||
AsyncMock(return_value=None)):
|
||||
from scribe.services.plugin_context import build_session_context
|
||||
out = await build_session_context(user_id=7, project_id=2)
|
||||
|
||||
assert "## Design system" not in out["context"]
|
||||
assert "## Active project: App (id 2)" in out["context"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
Reference in New Issue
Block a user