feat(design-systems): the model, the parent chain, and the guard on it
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 7s
CI & Build / integration (push) Successful in 18s
CI & Build / TypeScript typecheck (push) Successful in 20s
CI & Build / Python tests (push) Successful in 42s
CI & Build / Build & push image (push) Successful in 27s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 7s
CI & Build / integration (push) Successful in 18s
CI & Build / TypeScript typecheck (push) Successful in 20s
CI & Build / Python tests (push) Successful in 42s
CI & Build / Build & push image (push) Successful in 27s
Milestone #254 step 1 (#2286). A design system becomes a record Scribe holds rather than prose in a rulebook: a named set of tokens with an OPTIONAL parent, so a family system carries the house style and an app system carries only what it changes. Answering "what does this app alter?" is then `list its tokens` — nothing to compute. `parent_id` is the whole model. It replaces both an `always_on` flag (a family system is one with no parent) and a subscription join table (a project points at ONE system; the chain supplies the rest) — less schema than the rulebook shape it mirrors. Two decisions the task left open, settled here: - **Token values are JSONB keyed by mode**, not `value_light`/`value_dark` columns. The deciding argument was not flexibility, it was ambiguity: in a child system an unset mode means "inherit", in a root it means "not mode-dependent", and as columns both are NULL and the resolver cannot tell them apart. As a map, resolution is `{**parent, **child}` at every level with no special case for roots. Against it: queryability — but nothing filters tokens by value in SQL, so that buys a query no caller makes. - **`group_name` is free text, no CHECK enum.** Groupings are each design system's own vocabulary; a whitelist would bake one install's kit into the schema. No CHECK is introduced anywhere, so rule #36 does not fire. The cascade lives in `services/design_cascade.py` as pure functions over a `{id: parent_id}` map, importing nothing — which is what lets both the service and `access.py` use it without a cycle, and lets a test state a whole hierarchy in one literal. Cycles are refused on WRITE by walking up from the proposed parent (the cheap direction), and survived on READ by a visited-set, because a loop from a direct DB edit must truncate rather than hang. ACL (rule #78) is deliberately asymmetric: owning a system grants write, reaching one through a project you can see grants READ ONLY. An editor on a shared project must not be able to rewrite the family system every other project in that family resolves through. Also renames `services/design_system.py` -> `design_rulebook_import.py`. It is the #251 prose extractor, whose role is already scheduled to become a one-shot importer (#2288), and leaving it one character away from the new `design_systems.py` was a trap for every later session. Rule #115 throughout: nothing seeds a system or implies a default. An install with zero design systems is ordinary, not degraded.
This commit is contained in:
@@ -132,3 +132,40 @@ def test_clauses_are_pure_builders(clause_fn):
|
||||
import inspect
|
||||
assert not inspect.iscoroutinefunction(clause_fn)
|
||||
assert _sql(clause_fn(7)) # builds without touching a database
|
||||
|
||||
|
||||
# --- design systems ----------------------------------------------------------
|
||||
#
|
||||
# A design system is reachable two ways: you own it, or you can see a project
|
||||
# that inherits from it. The gap between what those two grant is the invariant
|
||||
# worth guarding — see get_design_system_permission.
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"permission, readable, writable",
|
||||
[
|
||||
("owner", True, True),
|
||||
# Project-derived. Being an EDITOR on a shared project must not confer
|
||||
# the right to rewrite the family system that project inherits from —
|
||||
# that would let one project's collaborator restyle every other project
|
||||
# in the family.
|
||||
("viewer", True, False),
|
||||
(None, False, False),
|
||||
],
|
||||
)
|
||||
async def test_reaching_a_design_system_via_a_project_reads_but_never_writes(
|
||||
permission, readable, writable
|
||||
):
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from scribe.services.access import (
|
||||
can_read_design_system,
|
||||
can_write_design_system,
|
||||
)
|
||||
|
||||
with patch(
|
||||
"scribe.services.access.get_design_system_permission",
|
||||
AsyncMock(return_value=permission),
|
||||
):
|
||||
assert await can_read_design_system(1, 3) is readable
|
||||
assert await can_write_design_system(1, 3) is writable
|
||||
|
||||
Reference in New Issue
Block a user