Skip to content

Commit f4252c9

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
fix(design): validate colour sinks and scope source ownership
1 parent 470abd9 commit f4252c9

13 files changed

Lines changed: 325 additions & 65 deletions

‎scripts/check-design-conformance-colour-assignments.test.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,3 +538,13 @@ test('unresolved layout-only aliases are not colour findings', () => {
538538
expect(report.unchecked.some((n) => n.file === css && n.reason.includes('--layout'))).toBe(true)
539539
expect(flagged({ [css]: '.label{--ink:var(--missing);color:var(--ink)}' })).toHaveLength(1)
540540
})
541+
542+
test('HSL channel tokens require a colour function at the visible sink', () => {
543+
const global = { [tokens]: ':root{--background:0 0% 100%;--paint:hsl(var(--background))}' }
544+
expect(flagged({ ...global, [css]: '.a{color:var(--background)}' })).toHaveLength(1)
545+
expect(flagged({ ...global, [css]: '.a{color:hsl(var(--background))}' })).toEqual([])
546+
expect(flagged({ ...global, [css]: '.a{color:var(--paint)}' })).toEqual([])
547+
expect(
548+
flagged({ ...global, [css]: '.a{--alias:var(--background);color:hsl(var(--alias))}' })
549+
).toEqual([])
550+
})

‎scripts/check-design-conformance-controls.test.ts‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,3 +237,25 @@ test('statically resolved ARIA buttons require a name', () => {
237237
).toHaveLength(0)
238238
expect(matching(run('<div role={role}/>'), 'control-accessible-name')).toHaveLength(0)
239239
})
240+
241+
test('directly referenced hidden text supplies an accessible name', () => {
242+
expect(
243+
matching(
244+
run(
245+
'<><span id="name" aria-hidden="true">Run</span><Button aria-labelledby="name"><X/></Button></>'
246+
),
247+
'control-accessible-name'
248+
)
249+
).toHaveLength(0)
250+
})
251+
test('undefined class alternatives cannot exempt visible unnamed buttons', () => {
252+
expect(
253+
matching(
254+
run('<Button className={visible ? undefined : "hidden"}><X/></Button>'),
255+
'control-accessible-name'
256+
)
257+
).toHaveLength(1)
258+
expect(
259+
matching(run('<Button className="hidden"><X/></Button>'), 'control-accessible-name')
260+
).toHaveLength(0)
261+
})

‎scripts/check-design-conformance-coverage.test.ts‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,3 +485,21 @@ test.each([
485485
])('inspection failures are not unresolved styling: %s', (reason) => {
486486
expect(inspectionFailure(reason)).toBe(true)
487487
})
488+
489+
test.each(['button', 'input', 'textarea', 'select', 'section > button.action', 'button + input'])(
490+
'bare native CSS controls are reviewable: %s',
491+
(selector) => {
492+
expect(
493+
inspect({
494+
'apps/sim/components/native.css': `${selector}{padding:3px}`,
495+
}).review.findings.some((f) => f.rule === 'styled-native-control')
496+
).toBe(true)
497+
}
498+
)
499+
test('similarly named CSS classes are not native-control selectors', () => {
500+
expect(
501+
inspect({ 'apps/sim/components/native.css': '.buttonish{padding:3px}' }).review.findings.some(
502+
(f) => f.rule === 'styled-native-control'
503+
)
504+
).toBe(false)
505+
})

‎scripts/check-design-conformance-typography.test.ts‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,3 +183,12 @@ test('uninspected CSS cannot establish global weight token ownership', () => {
183183
expect(result.classifications).toEqual([])
184184
expect(result.unchecked.some((n) => n.file === 'apps/sim/components/gap.css')).toBe(true)
185185
})
186+
187+
test('a runtime text-size writer revokes the loader fallback proof', () => {
188+
const result = review({
189+
[loader]: writer,
190+
[loaderCss]: labels,
191+
'apps/sim/components/writer.ts': "node.style.setProperty('--text-sm',runtime)",
192+
})
193+
expect(result.classifications.some((c) => c.file === loaderCss)).toBe(false)
194+
})

‎scripts/check-design-conformance.test.ts‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1004,3 +1004,33 @@ test('central inline artwork deletions retain an explicit artwork removal findin
10041004
)
10051005
expect(findings.some((f) => f.value === '(removed)')).toBe(true)
10061006
})
1007+
1008+
test('a sibling local variable cannot prove typography provenance', async () => {
1009+
const report = await diff(
1010+
'',
1011+
"const A=()=> <><span style={{'--type':'var(--text-small)'}}/><p style={{fontSize:'var(--type)'}}>Text</p></>"
1012+
)
1013+
expect(
1014+
report.findings.some((f) => f.rule === 'central-typography') ||
1015+
report.unchecked.some((n) => n.reason.includes('--type'))
1016+
).toBe(true)
1017+
})
1018+
test('policy object key order is quiet but array order remains meaningful', async () => {
1019+
const before = JSON.stringify({
1020+
version: '2.0.0',
1021+
limits: { depth: 12, branches: 64 },
1022+
scopes: ['a', 'b'],
1023+
})
1024+
const reordered = JSON.stringify({
1025+
scopes: ['a', 'b'],
1026+
limits: { branches: 64, depth: 12 },
1027+
version: '2.0.0',
1028+
})
1029+
const file = 'scripts/design-conformance/contracts.json'
1030+
expect((await diff(before, reordered, file)).findings).toEqual([])
1031+
expect(
1032+
(await diff(before, reordered.replace('["a","b"]', '["b","a"]'), file)).findings.some(
1033+
(f) => f.context === 'contract-registry'
1034+
)
1035+
).toBe(true)
1036+
})

‎scripts/design-conformance/conformance.ts‎

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -149,25 +149,23 @@ function expanded(ds: Declaration[], system: DesignSystem): Declaration[] {
149149
}
150150
function inspect(facts: Facts, file: string, system: DesignSystem): Checked {
151151
const out: Checked = { findings: [], unchecked: [...facts.unchecked], governed: 0, ungoverned: 0 }
152-
const localVars = new Map(
153-
(facts.surfaces ?? []).flatMap((s) =>
154-
s.atoms
152+
for (const surface of facts.surfaces ?? []) {
153+
const localVars = new Map(
154+
surface.atoms
155155
.filter((a) => a.kind === 'token' || a.property.startsWith('--'))
156156
.map((a) => [a.property, a.value] as const)
157157
)
158-
)
159-
const expand = (value: string, seen = new Set<string>()): string =>
160-
value.replace(/var\(\s*(--[\w-]+)\s*\)/g, (all, key: string) => {
161-
if (
162-
system.variableFamilies.has(key) ||
163-
!localVars.has(key) ||
164-
seen.has(key) ||
165-
seen.size >= 12
166-
)
167-
return all
168-
return expand(localVars.get(key) as string, new Set(seen).add(key))
169-
})
170-
for (const surface of facts.surfaces ?? []) {
158+
const expand = (value: string, seen = new Set<string>()): string =>
159+
value.replace(/var\(\s*(--[\w-]+)\s*\)/g, (all, key: string) => {
160+
if (
161+
system.variableFamilies.has(key) ||
162+
!localVars.has(key) ||
163+
seen.has(key) ||
164+
seen.size >= 12
165+
)
166+
return all
167+
return expand(localVars.get(key) as string, new Set(seen).add(key))
168+
})
171169
if (surface.kind === 'integration') continue
172170
const surfaceFindings = new Set<string>()
173171
const emit = (
@@ -684,15 +682,27 @@ export class ConformanceLinter {
684682
if (isRegistry(file)) {
685683
const b = change.before ? JSON.parse(read(change.before)) : null
686684
const a = change.after ? JSON.parse(read(change.after)) : null
687-
if (canonical(b) !== canonical(a))
685+
const stable = (value: unknown): unknown =>
686+
Array.isArray(value)
687+
? value.map(stable)
688+
: value && typeof value === 'object'
689+
? Object.fromEntries(
690+
Object.entries(value)
691+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
692+
.map(([key, value]) => [key, stable(value)])
693+
)
694+
: value
695+
const beforePolicy = canonical(stable(b))
696+
const afterPolicy = canonical(stable(a))
697+
if (beforePolicy !== afterPolicy)
688698
report.findings.push({
689699
kind: 'system-change',
690700
contract: 'central-definition',
691701
rule: 'central-definition',
692702
category: 'contracts',
693703
property: 'registry',
694-
value: hash(canonical(a)),
695-
before: hash(canonical(b)),
704+
value: hash(afterPolicy),
705+
before: hash(beforePolicy),
696706
reason: 'Design-system contract registry changed',
697707
file,
698708
line: 1,

0 commit comments

Comments
 (0)