Skip to content

Commit 10b25f9

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
fix(design): report inspection gaps and harden source tracing
1 parent b781ae8 commit 10b25f9

51 files changed

Lines changed: 614 additions & 84 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.agents/skills/emcn-design-review/SKILL.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ argument-hint: "[scope] [fix=true|false]"
66

77
# EMCN design review
88

9-
Review the requested product UI scope (default: current changes). When `fix=false`, explain proposed changes without applying them.
9+
User arguments: $ARGUMENTS
10+
11+
Interpret the arguments as the product UI scope (default: current changes) and an optional `fix=true|false` mode (default: `false`). When `fix=false`, explain proposed changes without applying them.
1012

1113
1. When EMCN, global styles, recipes or design ownership metadata change, run `bun run design:generate` and commit `scripts/design-conformance/contracts.generated.json` with the source. `bun run check:design-generated` checks freshness without writing. Regeneration does not hide the originating design-system finding.
1214
2. During UI work, run `bun run check:design --base origin/staging --working-tree` from the repo root, substituting the actual PR target for `origin/staging`. After committing, use `--head HEAD` for the immutable PR comparison. Exit 1 means findings to review; exit 2 means the check failed and must be repaired or reported. CI is warning-only for findings and fails on incomplete analysis.

‎bun.lock‎

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@
1818
"test:setup": "bun run --cwd packages/sim-setup test",
1919
"test:integration": "bun --no-env-file scripts/test-integration.ts",
2020
"test:search-performance": "KNOWLEDGE_SEARCH_PERFORMANCE_TEST=true bun --no-env-file scripts/test-integration.ts search-latency",
21-
"format": "biome format --write scripts/design-conformance scripts/check-design-conformance*.ts && turbo run format",
22-
"format:check": "biome format scripts/design-conformance scripts/check-design-conformance*.ts && turbo run format:check",
21+
"format": "biome format --write scripts/design-conformance scripts/design-scan scripts/design-studio tools/design-studio scripts/check-design-conformance*.ts scripts/generate-design-contracts*.ts && turbo run format",
22+
"format:check": "biome format scripts/design-conformance scripts/design-scan scripts/design-studio tools/design-studio scripts/check-design-conformance*.ts scripts/generate-design-contracts*.ts && turbo run format:check",
2323
"lint": "biome check --write scripts/design-conformance scripts/design-scan scripts/design-studio tools/design-studio scripts/check-design-conformance*.ts scripts/generate-design-contracts*.ts && turbo run lint",
2424
"lint:check": "biome check scripts/design-conformance scripts/design-scan scripts/design-studio tools/design-studio scripts/check-design-conformance*.ts scripts/generate-design-contracts*.ts && turbo run lint:check",
2525
"lint:helm": "helm lint helm/sim --strict --values helm/sim/ci/default-values.yaml",
@@ -173,13 +173,16 @@
173173
"@octokit/rest": "^21.0.0",
174174
"@sim/utils": "workspace:*",
175175
"@types/babel__traverse": "7.28.0",
176+
"@types/aria-query": "5.0.4",
176177
"@types/bun": "1.4.1",
177178
"@types/node": "24.2.1",
178179
"@types/opentype.js": "1.3.10",
179180
"@typescript/native": "npm:typescript@^7.0.2",
180181
"@typescript/typescript6": "^6.0.2",
181182
"@vercel/og": "0.6.8",
182183
"ajv": "8.18.0",
184+
"aria-query": "5.3.2",
185+
"color-name": "1.1.4",
183186
"commander": "^11.1.0",
184187
"concurrently": "10.0.5",
185188
"drizzle-orm": "^0.45.2",

‎packages/emcn/src/components/chip-modal/chip-modal.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -774,7 +774,7 @@ export type ChipModalFieldProps =
774774
* static permission list). For a multi-email chip-list input, prefer
775775
* `type='emails'` over a `type='custom'` `TagInput` wrapper — it internalizes
776776
* chip rendering, dedupe, format validation, paste, and Backspace handling.
777-
* @designProtect className *
777+
* @designProtect className colours typography padding height gap border-radius borders box-shadow flex-direction align-items justify-content
778778
*/
779779
function ChipModalField(props: ChipModalFieldProps) {
780780
const id = React.useId()

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

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/** biome-ignore-all lint/suspicious/noTemplateCurlyInString: Fixtures contain literal proposed JavaScript templates. */
22
import { existsSync } from 'node:fs'
33
import { beforeAll, expect, test } from 'vitest'
4+
import { inspectControls } from '#control-analysis/inventory'
45
import { extract } from '#design-conformance/extract'
56
import { loadCatalogue } from '#design-conformance/io'
67
import { Linter } from '#design-conformance/lint'
@@ -413,3 +414,37 @@ test('tag differences are not blanket-equated for text-bearing elements', () =>
413414
diff('<span className="p-2">Label</span>', '<div className="p-2">Label</div>').flagged
414415
).toBe(true)
415416
})
417+
418+
test('changing one of repeated styled surfaces retains its previous appearance', () => {
419+
const r = diff(
420+
'export const A=()=> <><button className="p-2"/><button className="p-2"/></>',
421+
'export const A=()=> <><button className="p-2"/><button className="p-4"/></>'
422+
)
423+
expect(r.findings.some((f) => f.rule === 'appearance-changed' && f.before !== null)).toBe(true)
424+
})
425+
426+
test('appearance findings retain direct attribution to the cited consumer slot', () => {
427+
const source = 'export const A=()=> <button className="p-4">Save</button>'
428+
const report = diff(source.replace('p-4', 'p-2'), source)
429+
const findings = report.findings.map((f, index) => ({
430+
...f,
431+
id: String(index),
432+
observedFrom: [ui],
433+
}))
434+
const result = inspectControls(
435+
{
436+
entries: [
437+
{
438+
path: ui,
439+
blob: 'a'.repeat(40),
440+
mode: '100644',
441+
kind: 'blob',
442+
bytes: Buffer.byteLength(source),
443+
},
444+
],
445+
read: () => source,
446+
},
447+
findings
448+
)
449+
expect(result.records[0].findingIds).toEqual(findings.map((f) => f.id))
450+
})

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,3 +510,20 @@ test('literal joins and unshadowed String.raw resolve class assignments without
510510
}).length
511511
).toBeGreaterThan(0)
512512
})
513+
514+
test('a global shadow must contain a complete shadow recipe, not just a named variable', () => {
515+
for (const definition of ['8px', 'red', 'var(--text-body)', '0 0 2px var(--missing)']) {
516+
const report = inspect({
517+
[tokens]: `${globals}:root{--shadow-card:${definition}}`,
518+
[ui]: 'export const A=()=> <span style={{boxShadow:"var(--shadow-card)"}}/>',
519+
})
520+
expect(report.verifiedUsages).toEqual([])
521+
expect([...report.findings, ...report.unchecked].length).toBeGreaterThan(0)
522+
}
523+
expect(
524+
inspect({
525+
[tokens]: `${globals}:root{--shadow-card:0 1px 2px #000}`,
526+
[ui]: 'export const A=()=> <span style={{boxShadow:"var(--shadow-card)"}}/>',
527+
}).assignments.some((a) => a.name === 'box-shadow' && a.status === 'verified')
528+
).toBe(true)
529+
})

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,3 +444,17 @@ test('public diff checker separates layout allowances and exposes a later intern
444444
expect(changed.findings.some((f) => f.value === 'p-4')).toBe(true)
445445
expect(changed.findings.some((f) => f.value === 'flex-row')).toBe(false)
446446
})
447+
448+
test('the public diff includes local custom-property writes despite legacy token-policy exemptions', async () => {
449+
const repo = mkdtempSync(path.join(temp, 'local-writer-'))
450+
git(repo, ['init', '-q'])
451+
put(repo, 'apps/sim/app/_styles/globals.css', ':root{--text-body:#444}')
452+
put(repo, ui, 'export const A=()=> <span>Label</span>')
453+
const base = commit(repo)
454+
put(repo, ui, 'export const A=()=> <span style={{"--text-body":"red"}}>Label</span>')
455+
const head = commit(repo)
456+
const report = await checkComparison({ repo, base, head, policy: 'conformance' })
457+
expect(
458+
report.findings.some((f) => f.rule === 'central-colour-assignment' && f.value.includes('red'))
459+
).toBe(true)
460+
})

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/** biome-ignore-all lint/suspicious/noTemplateCurlyInString: Fixtures contain proposed source text. */
12
import { readFileSync } from 'node:fs'
23
import { expect, test } from 'vitest'
34
import type { ControlSource } from '#control-analysis/model'
@@ -409,3 +410,24 @@ test('syntax and extraction errors remain distinguishable', () => {
409410
false
410411
)
411412
})
413+
414+
test('rendered CSS template substitutions remain visible as unchecked analysis', () => {
415+
const report = inspect({
416+
[ui]: 'const html=`<style>body{color:${color}}</style>`;new Blob([html],{type:"text/html"})',
417+
})
418+
expect(report.review.unchecked.some((n) => /CSS|substitut/i.test(n.reason))).toBe(true)
419+
})
420+
421+
test('hidden false and runtime visibility cannot hide local control styling', () => {
422+
for (const value of ['false', 'visible']) {
423+
const report = inspect({
424+
[ui]: `export const A=()=> <button hidden={${value}} className="rounded-xl bg-red-500">Go</button>`,
425+
})
426+
expect(report.review.findings.some((f) => f.rule === 'local-control')).toBe(true)
427+
}
428+
expect(
429+
inspect({
430+
[ui]: 'export const A=()=> <button hidden className="rounded-xl bg-red-500">Go</button>',
431+
}).review.findings.some((f) => f.rule === 'local-control')
432+
).toBe(false)
433+
})

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

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { execFileSync, spawnSync } from 'node:child_process'
1+
import { spawnSync } from 'node:child_process'
22
import { createHash } from 'node:crypto'
33
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
44
import os from 'node:os'
@@ -170,9 +170,9 @@ test('single and multi-process evaluation match on synthetic stored data', () =>
170170
)
171171
expect(storedChanges(dataset, c)).toHaveLength(1)
172172
}
173-
const run = (workers: number) => {
174-
const out = path.join(repo, `evaluation-${workers}`)
175-
execFileSync(
173+
const run = (workers: number, expectedExit = 0) => {
174+
const out = path.join(repo, `evaluation-${workers}-${expectedExit}`)
175+
const result = spawnSync(
176176
'bun',
177177
[
178178
'--no-env-file',
@@ -188,6 +188,7 @@ test('single and multi-process evaluation match on synthetic stored data', () =>
188188
],
189189
{ stdio: ['ignore', 'pipe', 'pipe'] }
190190
)
191+
expect(result.status).toBe(expectedExit)
191192
return out
192193
}
193194
const one = run(1)
@@ -196,4 +197,33 @@ test('single and multi-process evaluation match on synthetic stored data', () =>
196197
expect(readFileSync(path.join(one, `${pr}.json`), 'utf8')).toBe(
197198
readFileSync(path.join(two, `${pr}.json`), 'utf8')
198199
)
200+
writeFileSync(path.join(dataset, 'objects', `${blob}.gz`), gzipSync('tampered input'))
201+
for (const workers of [1, 2]) {
202+
const output = run(workers, 2)
203+
const summary = JSON.parse(readFileSync(path.join(output, 'evaluation.json'), 'utf8'))
204+
expect(summary.failed).toEqual([1, 2])
205+
for (const pr of [1, 2])
206+
expect(JSON.parse(readFileSync(path.join(output, `${pr}.json`), 'utf8')).status).toBe(
207+
'failed'
208+
)
209+
}
210+
})
211+
212+
test('unwritable CLI output remains an operational exit, not an uncaught retry', () => {
213+
const result = spawnSync('bun', [
214+
'--no-env-file',
215+
cli,
216+
'--policy',
217+
'appearance',
218+
'--repo',
219+
repo,
220+
'--base',
221+
base,
222+
'--head',
223+
head,
224+
'--output',
225+
repo,
226+
])
227+
expect(result.status).toBe(2)
228+
expect(result.stderr.toString()).toContain('failed')
199229
})

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,9 @@ test('registered source definitions integrate with snapshots and system-change r
8686
expect(unresolved.flagged).toBe(false)
8787
expect(unresolved.unchecked.some((n) => n.reason.includes(name))).toBe(true)
8888
})
89+
90+
test('removing an unresolved registered recipe still reports the source deletion', () => {
91+
expect(recipeDiff(file, parse(source('unknown()')), parse(''))).toMatchObject([
92+
{ value: '(registered export removed)', before: 'unknown()' },
93+
])
94+
})

0 commit comments

Comments
 (0)