"""Rulebook prose -> a PROPOSED design system (milestone #254 step 3). The extraction tested in test_design_rulebook_import.py answers "what claims does this rulebook make". This answers a harder question — "what design system is it describing" — which needs the two halves joined: one rule names the colours, another names the custom properties, and neither alone is a token. Rule text is representative rather than copied from this operator's rulebook (rule #115): a test that only passes against their exact wording would be testing the instance. """ from types import SimpleNamespace from scribe.services.design_rulebook_import import propose_tokens def _rule(rule_id, title, statement, how_to_apply=None): return SimpleNamespace( id=rule_id, title=title, statement=statement, how_to_apply=how_to_apply ) SURFACES = _rule( 51, "Universal surfaces", "Obsidian #14171A (page bg, deepest surface), Iron #1E2228 (cards), " "Slate #2C313A (hovered surfaces).", ) TEXT = _rule( 52, "Text palette", "Text tokens: Parchment #E8E4D8 (primary text), Vellum #C2BFB4 (secondary). " "Pure white #FFFFFF is NEVER used as text color.", ) PROPERTIES = _rule( 72, "CSS custom properties", "Expose the system as custom properties: surfaces " "(--fs-obsidian/iron/slate), text (--fs-parchment/vellum), and radius " "(--fs-radius-sm/md/lg).", ) def _by_name(proposals): return {p.name: p for p in proposals} # --- the join --------------------------------------------------------------- def test_a_token_takes_its_value_from_the_colour_of_the_same_name(): """THE mechanism. `--fs-obsidian` and "Obsidian #14171A" are declared in different rules and neither is a token on its own. Joining them on the word is the only reason an import produces something usable instead of a list of empty names.""" proposals = _by_name(propose_tokens([SURFACES, PROPERTIES])) assert proposals["--fs-obsidian"].value_by_mode == {"base": "#14171a"} assert proposals["--fs-iron"].value_by_mode == {"base": "#1e2228"} def test_the_parenthetical_becomes_the_tokens_purpose(): """Rulebooks say what a colour is FOR right beside its value, and that is the field a bare hex can never carry.""" proposals = _by_name(propose_tokens([SURFACES, PROPERTIES])) assert proposals["--fs-obsidian"].purpose == "page bg, deepest surface" def test_a_token_with_no_matching_colour_is_proposed_with_no_value(): """HONEST OUTPUT, not a failure. The rulebook states radius steps as prose ("Small 4px"), which nothing here parses. The name is real and the value needs a human — proposing the name with an empty value says exactly that, where dropping it would hide a token the rulebook asked for.""" proposals = _by_name(propose_tokens([SURFACES, PROPERTIES])) assert proposals["--fs-radius-sm"].value_by_mode == {} assert "--fs-radius-lg" in proposals def test_every_proposal_carries_the_rule_and_sentence_it_came_from(): """An import is a proposal a human reviews, and a claim you cannot trace is a claim you have to take on faith.""" obsidian = _by_name(propose_tokens([SURFACES, PROPERTIES]))["--fs-obsidian"] assert obsidian.source_rule_id == 51 assert obsidian.source_rule_title == "Universal surfaces" assert "Obsidian #14171A" in obsidian.source_context # --- prohibitions become replacements --------------------------------------- def test_a_prohibition_becomes_supersedes_on_that_rules_primary_token(): """The reframe, end to end. Rule 52 declares Parchment and forbids pure white in one breath; the import turns that into "write --fs-parchment instead of #ffffff" — the same fact, stated forwards, and actionable.""" proposals = _by_name(propose_tokens([TEXT, PROPERTIES])) assert proposals["--fs-parchment"].supersedes == ["#ffffff"] def test_a_prohibition_attaches_to_one_token_not_every_token_of_its_rule(): """Rule 52 declares two colours. Attaching the prohibition to both would claim the rulebook said something it didn't — that Vellum is also the replacement for white.""" proposals = _by_name(propose_tokens([TEXT, PROPERTIES])) assert proposals["--fs-vellum"].supersedes == [] def test_a_forbidden_colour_never_becomes_a_token_value(): """Sentence-scoped negation carried through to the import: #FFFFFF appears in rule 52 as a hex, and a naive read would make it Parchment's value.""" proposals = propose_tokens([TEXT, PROPERTIES]) for proposal in proposals: assert proposal.value_by_mode.get("base") != "#ffffff" # --- grouping --------------------------------------------------------------- def test_a_family_token_is_grouped_by_its_middle_segment(): """`--fs-radius-sm` -> "radius". Structural, so it works on a naming scheme this code has never seen — the prefix is each install's own (rule #115).""" proposals = _by_name(propose_tokens([SURFACES, PROPERTIES])) assert proposals["--fs-radius-sm"].group_name == "radius" def test_a_flat_token_is_left_ungrouped_rather_than_guessed_at(): proposals = _by_name(propose_tokens([SURFACES, PROPERTIES])) assert proposals["--fs-obsidian"].group_name is None # --- shape ------------------------------------------------------------------ def test_each_token_name_is_proposed_exactly_once(): """The slash shorthand expands and rules repeat colours; neither may produce a duplicate, since two live rows with one name is the duplicate-definition bug the unique index exists to refuse.""" names = [p.name for p in propose_tokens([SURFACES, TEXT, PROPERTIES])] assert len(names) == len(set(names)) def test_a_rulebook_that_names_no_tokens_proposes_nothing(): """Most rulebooks are not design rulebooks. That has to be an empty result rather than an error — an install can point this at anything.""" unrelated = _rule(1, "Branching", "Work happens on the dev branch.") assert propose_tokens([unrelated]) == [] def test_colours_declared_without_a_token_name_are_not_invented_into_tokens(): """A rulebook naming a colour it never exposes as a custom property has not asked for a token, and inventing a name for it would put a token in the record that no rule sanctions.""" proposals = propose_tokens([SURFACES]) assert proposals == []