Commit Graph
3 Commits
Author SHA1 Message Date
bvandeusen 3da40abcb8 feat(design-systems): declare what to write instead, rather than what not to
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 34s
CI & Build / Python tests (push) Successful in 43s
CI & Build / Build & push image (push) Successful in 38s
Milestone #254 step 6, first half (#2295) — and this reframes the task rather
than answering it. The operator's call:

  "in this case we should declare what should be used in place of pure white,
   it's not a prohibition it's what should be used in its place."

None of the three options on the table (a constraints record / the panel reads
both sources / negative token rows) was right, because all three kept the
prohibition as a KIND OF THING. It isn't one. "Pure white is never text" is the
shadow cast by a positive fact — text is Parchment — and a design system that
stores what things ARE has no row for a ban because it never needed one.

So `design_tokens` gains `supersedes`: the literal values this token should be
written instead of. `--color-text-on-action` supersedes `#fff` / `#ffffff`. Same
fact as the rule, stated forwards, and now actionable — a finding can say what
to write rather than only objecting.

It has to be DECLARED, not derived, and that is the crux: `#fff` and Parchment
`#E8E4D8` are different colours, so no value-matching check could ever have
connected them. That mismatch is precisely why the prohibition looked
unrepresentable until it was turned around.

`supersedes` cascades on EMPTINESS rather than on None. A child overriding a
colour says nothing about which literals it replaces, and blanking the family's
declaration there would silently disarm the check for every app that customises
the token — while a child that states its own list replaces it wholesale.

Two things this deliberately does NOT do:

  - It does not feed the drift panel. Superseded literals live in component CSS,
    which `designDrift.ts` cannot see and already documents as a blind spot.
    This is input for the source lint (#2277). Declaring it with nothing
    consuming it yet is honest; wiring it to a panel that cannot check it would
    not be.
  - It does not remove the panel's `prohibited_color` arm yet — that happens
    when the panel is repointed at a resolved system, which needs #2288 first.

The declaration also exposes a missing token: most of the 67 hardcoded
`color: #fff` (#2275) are text on a coloured action button, and the system has
no token for that role at all. Every view hardcodes it. Declaring the token that
was never there is the first real output of the operator's framing.
2026-07-30 21:16:45 -04:00
bvandeusen 839d6902ad feat(design-systems): resolve the chain, and keep the argument not the verdict
CI & Build / integration (push) Successful in 15s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 42s
CI & Build / Build & push image (push) Successful in 29s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 8s
Milestone #254 step 2 (#2287). `resolve_tokens` flattens a system's inheritance
chain into its effective token set — walk to the root, deepest wins by token
name. Pure and duck-typed, so a test states a whole hierarchy in literals and
the service hands the same function ORM rows.

**Provenance is stored as the contest, not the winner.** A ResolvedToken carries
every system that offered a value, per mode, deepest first — `[0]` won and
`[1:]` are what it shadowed. "Which system supplied this?" and "what did it
override?" are then two reads of one list and cannot disagree, where a winner
plus a separate provenance field would be two things to keep in step.

**Merging is per (name, MODE), and that is the storage decision paying off.** A
system that deepens one accent for light backgrounds while leaving dark alone
owns `base` and still inherits `dark`. A token-level "overridden here" flag
would have to lie about one of them, and the two-column shape could not have
represented it at all.

Metadata cascades separately by the same deepest-wins rule, with one exception:
`order_index` treats 0 as UNSTATED rather than "first", because 0 is the column
default. Reading it as a real value would let a colour-only override drag its
token to the top of its group — a visible reshuffle in return for a change that
touched nothing structural.

One fix to step 1 while wiring this up: `_parent_map` is now scoped to the
SYSTEM'S OWNER rather than the caller. A caller reading through a shared project
owns no link in the chain, so the caller-scoped version would have handed them
an empty forest and truncated the cascade to a single system — a page rendering
with plausible wrong values and no error anywhere. The ACL already grants read
along the whole chain; this is the loading side keeping that promise, and it now
has a test naming the shared-project case.

`BASE_MODE` moves from the model to the cascade module, where it belongs: it is
a resolution rule, not a storage fact, and design_cascade.py deliberately
imports nothing so both access.py and the service can depend on it.
2026-07-30 17:02:31 -04:00
bvandeusen 03b3998585 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 / Python tests (push) Successful in 42s
CI & Build / integration (push) Successful in 18s
CI & Build / TypeScript typecheck (push) Successful in 20s
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.
2026-07-30 16:54:41 -04:00