Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions docs/near-miss.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,22 @@ suite is a weaker record than a red check for the purpose this document serves,
and it is a stronger one for keeping the pair true as the table changes, so both
are named.

One bound on that, because it is the shape a pair goes stale in without anybody
editing it. The fixture map holds one violation per row, so what it proves is
that the row refused something, never which branch of the operator behind it did
the refusing. A row reading several shapes can lose one and stay green under it.
The row about where a token value is read from is the one that has several: it
reads a colour, a length, a font family and a font weight, and its entry in the
map is a colour. What holds the other three is a pair of its own, one fixture per
shape and one line per thing the row has to walk past:

go test ./internal/invariant -run 'TestTheTokenRow' -count=1

The same goes for the two rows whose permitted values are tested separately, the
loopback addresses a test may bind and the version-shaped numbers a workflow may
carry. Where a row is named here with no such test beside it, one violation is
the whole of what it has been shown to refuse.

## Reproducible build

It refuses a source whose two builds do not produce the same bytes.
Expand Down
71 changes: 64 additions & 7 deletions internal/invariant/invariant.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,25 @@ var (
// colour.
hexColour = regexp.MustCompile(`(?i)#(?:[0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{4}|[0-9a-f]{3})\b`)
fragmentReference = regexp.MustCompile(`(?is)\b(?:href|src)\s*=\s*"#[^"]*"`)
// A number carrying one of the units a length is written in on the web.
// The token file states its lengths as bare numbers of
// density-independent pixels and says so in its own how-to-read-this
// block, so what a page writes is that number with a unit stuck to it,
// and the unit is what tells a length in a stylesheet apart from a
// number in a sentence. The digits have to touch the unit: a prose
// sentence putting a space between a figure and a word is not a length,
// and a word ending in one of these letters carries no digits in front
// of it.
cssLength = regexp.MustCompile(`(?i)\b[0-9]+(?:\.[0-9]+)?(?:px|rem|em|ch|vh|vw|pt)\b`)
// The two declarations the file's font stacks and type weights are
// spelled as. The property rather than the value, because a stack is a
// list somebody shortens and a weight is three digits that look like
// nothing in particular, and neither is recognisable on its own the way
// a hex colour is. What precedes the name is part of the pattern for the
// reason the colour scheme property carries one: a hyphen in front of it
// is a different property.
fontFamilyProperty = regexp.MustCompile(`(?is)(^|[^-a-z])font-family\s*:`)
fontWeightProperty = regexp.MustCompile(`(?is)(^|[^-a-z])font-weight\s*:`)
// A run of digits separated by dots, which is every version-shaped thing
// in a line. The row compares each whole run against the one version
// this repository holds rather than searching for that version inside a
Expand Down Expand Up @@ -432,9 +451,9 @@ func Rules() []Rule {
{
ID: "design-tokens-live-in-exactly-one-file",
Subject: BuildInputs,
Reason: "a colour typed into a template is a second definition of a value published somewhere else, and the day the published one moves the page goes on rendering the old one perfectly, so nobody sees it",
Refuses: "a colour written into what the build reads, in any of the hex forms the published file uses, outside a fragment reference",
decide: decideTypedColour,
Reason: "a value typed into a template is a second definition of a value published somewhere else, and the day the published one moves the page goes on rendering the old one perfectly, so nobody sees it; the file carries lengths, weights and font stacks beside the colours, and a wrong length is the harder one to see because a wrong colour at least looks wrong",
Refuses: "a colour, a length, a font family or a font weight written into what the build reads, outside a fragment reference",
decide: decideTypedTokenValue,
},
{
ID: "version-lives-in-exactly-one-file",
Expand Down Expand Up @@ -776,7 +795,30 @@ func decideSecondVersion(body []byte) []string {
return details
}

func decideTypedColour(body []byte) []string {
// decideTypedTokenValue refuses a value the token file is the authority for,
// written into something the build reads.
//
// It reads a shape per group of values the file carries rather than the colours
// alone. The row's name has always been about tokens and what it decided was
// about colour, and the gap is easy to miss for the reason it exists: a colour
// typed next to the thing it describes is visible to anybody who opens the page
// afterwards with the published file beside it, and a type size, a corner radius
// or a font stack typed the same way is not. The page that demonstrates the
// design system is where all four get written next to what they describe, which
// is why the shapes are here before that page rather than after it.
//
// The bound is the shape of the literal rather than its meaning, which is the
// bound the version row declares for itself and for the same reason. A hex run
// is a colour, digits touching a unit are a length, and the two font
// declarations are named by their property because a stack and a three-digit
// weight are not recognisable on their own. A value spelled any other way walks
// through, so the file stays the authority for the set rather than this row.
//
// What it deliberately does not read is a fragment reference. An address ending
// in a name of hex length is a link to a place on the page and not a colour, and
// a row that could not tell the two apart would refuse the link the frame is
// built around.
func decideTypedTokenValue(body []byte) []string {
var details []string
for i, line := range strings.Split(string(body), "\n") {
text := fragmentReference.ReplaceAllString(line, "")
Expand All @@ -785,6 +827,21 @@ func decideTypedColour(body []byte) []string {
"line %d writes the colour %s, and %s is the one file a colour is read from",
i+1, m, tokens.File))
}
for _, m := range cssLength.FindAllString(text, -1) {
details = append(details, fmt.Sprintf(
"line %d writes the length %s, and %s is the one file a length is read from",
i+1, m, tokens.File))
}
if fontFamilyProperty.MatchString(text) {
details = append(details, fmt.Sprintf(
"line %d declares a font family, and %s is the one file a font stack is read from",
i+1, tokens.File))
}
if fontWeightProperty.MatchString(text) {
details = append(details, fmt.Sprintf(
"line %d declares a font weight, and %s is the one file a weight is read from",
i+1, tokens.File))
}
}
return details
}
Expand Down Expand Up @@ -950,16 +1007,16 @@ func gather(root string) (map[string][]file, error) {
return nil, err
}

// The rule about where a colour is read from is a rule about there being
// exactly one such file, and a run that decided it against a tree
// The rule about where a token value is read from is a rule about there
// being exactly one such file, and a run that decided it against a tree
// carrying none would report that no second definition was found in a
// tree with no first one. So the copy being absent is refused here, by
// name, rather than passing as a row with nothing to compare.
if len(tracked.securitySources) == 0 {
return nil, fmt.Errorf("%s is not tracked in this tree, so the build wrote no %s and the row about an expired reporting route has nothing to read", security.File, security.Path)
}
if len(tracked.tokenCopies) == 0 {
return nil, fmt.Errorf("%s is not tracked in this tree, and the row about where a colour is read from is a row about there being exactly one such file", tokens.File)
return nil, fmt.Errorf("%s is not tracked in this tree, and the row about where a token value is read from is a row about there being exactly one such file", tokens.File)
}

return map[string][]file{
Expand Down
56 changes: 54 additions & 2 deletions internal/invariant/invariant_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,58 @@ func TestTheReleaseVersionRowTellsAnotherVersionApart(t *testing.T) {
}
}

// Every group of values the token file carries, in the shape it reaches a build
// input in. The row is named for the file rather than for one of its groups, so
// a group it walks past is a value somebody may type next to the thing it
// describes with nothing to stop them, and the page that describes all four is
// the page where that happens.
//
// Each fixture is one line of a stylesheet somebody writes while looking at the
// published value, which is the whole of this mistake: it renders correctly, it
// keeps rendering correctly after the published value moves, and the only party
// who finds out is a client built from the file.
func TestTheTokenRowRefusesEveryShapeTheFileCarries(t *testing.T) {
for name, fixture := range map[string]struct{ line, names string }{
"a colour out of the surface group": {` <style>body { background: #121216 }</style>`, "#121216"},
"a type size": {` <style>h1 { font-size: 56px }</style>`, "56px"},
"a corner radius": {` <style>.tile { border-radius: 12px }</style>`, "12px"},
"the width a column stops at": {` <style>main { max-width: 1080px }</style>`, "1080px"},
"a font stack": {` <style>body { font-family: ui-sans-serif, sans-serif }</style>`, "font stack"},
"a type weight": {` <style>h1 { font-weight: 700 }</style>`, "weight"},
} {
got := decideTypedTokenValue([]byte(fixture.line + "\n"))
if len(got) == 0 {
t.Errorf("%s was not refused", name)
continue
}
if !strings.Contains(got[0], fixture.names) {
t.Errorf("%s was refused without naming %q: %v", name, fixture.names, got)
}
if !strings.Contains(got[0], tokens.File) {
t.Errorf("%s was refused without naming where the value is read from: %v", name, got)
}
}
}

// What the row leaves alone, and every line here is one this tree carries or
// would carry. A row that refused one of them reds a clean build input for
// something that is not a token, which is how a row gets taken back out.
func TestTheTokenRowTellsAValueApartFromASentence(t *testing.T) {
for name, line := range map[string]string{
"the link the frame is built around": ` <a href="#content">Skip to the content</a>`,
"a figure with its unit spelled out": "The published page is read from 35 cm and a television from 3 m.",
"a word that ends in a unit": "Nothing here is a problem anybody has to solve.",
"a number in a sentence": "Twelve plugins, and 1080 of the lines below are prose.",
"a date": "Run 2026-08-13 against the default branch.",
"a property that only ends in one": ` <style>.tile { scroll-padding: var(--r) }</style>`,
"a value taken from the token itself": ` <style>h1 { font-size: var(--display) }</style>`,
} {
if got := decideTypedTokenValue([]byte(line + "\n")); len(got) != 0 {
t.Errorf("the row refused %s: %v", name, got)
}
}
}

// The copy at the end of a sentence, which is how a document actually writes a
// version, and the refusal has to say where the version belongs or the next
// person deletes the sentence instead of reading it from the one file.
Expand Down Expand Up @@ -1196,7 +1248,7 @@ func TestTheColourRowRefusesATypedColourAndLeavesAFragmentAlone(t *testing.T) {
"a colour in the prose the build reads": `The accent is #5B9CFF on a dark ground.`,
}
for name, line := range refused {
got := decideTypedColour([]byte(line))
got := decideTypedTokenValue([]byte(line))
if len(got) != 1 {
t.Errorf("the colour row did not refuse %s: %v", name, got)
continue
Expand All @@ -1213,7 +1265,7 @@ func TestTheColourRowRefusesATypedColourAndLeavesAFragmentAlone(t *testing.T) {
"a run of digits that is five": `<p>Case #12345 is closed.</p>`,
}
for name, line := range spared {
if got := decideTypedColour([]byte(line)); len(got) != 0 {
if got := decideTypedTokenValue([]byte(line)); len(got) != 0 {
t.Errorf("the colour row refused %s: %v", name, got)
}
}
Expand Down
Loading