Skip to content

Commit 338b30b

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
refactor(design): publish source contracts in scan report
1 parent bbb9048 commit 338b30b

36 files changed

Lines changed: 289 additions & 1082 deletions

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ User arguments: $ARGUMENTS
1010

1111
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.
1212

13-
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.
13+
1. When EMCN, global styles, recipes or design ownership metadata change, run the diff check in step 2. It derives facts from each source revision, so the originating design-system change remains visible without a committed metadata file.
1414
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.
1515
3. For each new finding, inspect the cited source, the applicable public EMCN export in `packages/emcn/src/index.ts`, and tokens and recipes in `apps/sim/app/_styles/globals.css`. Reuse a suitable component, prop, variant, or global token when it expresses the design intent. Avoid near-duplicate local colours or overriding EMCN chrome merely for convenience.
1616
4. A genuinely new product treatment may remain an Extra. Explain its visual intent and why existing EMCN or global styling does not fit in the PR. The check does not decide design approval and must not be silenced by adding an arbitrary token, broad exclusion, or fake component wrapper. Ask the designer or engineer when changing a shared recipe would have broad or ambiguous effects.

‎.agents/skills/ship/SKILL.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ When the user runs `/ship`:
102102
103103
## Committed design check
104104
105-
When central EMCN sources, global styles, recipes or `@designAllow`/`@designProtect` metadata change, run `bun run design:generate`, review the result and commit `contracts.generated.json` alongside the source. `check:design-generated` is part of `check:audits`; stale output is an infrastructure error. Regeneration does not suppress the source finding.
105+
When central EMCN sources, global styles, recipes or `@designAllow`/`@designProtect` metadata change, run the design diff check and review its central-system findings. The checker derives metadata independently from the base and proposed source; no generated artifact needs to be committed.
106106
107107
During product UI work, run `bun run check:design --base origin/staging --working-tree` so staged, unstaged and nonignored new files are included. Review findings against EMCN and `globals.css`; explain intentional new Extras rather than weakening the checker. After committing and before **every push**, run this from the repository root with the repository-pinned Bun version:
108108

‎.claude/rules/emcn-components.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ The menu surface intentionally diverges from the pill: `dropdown-menu.tsx` items
2121

2222
## Public components and design behavior
2323

24-
The current public exports, variants, defaults, styling slots, and relationships are generated from source in `scripts/design-conformance/contracts.generated.json`. Run `bun run design:generate` when EMCN or `globals.css` changes. Browse and interact with the visual exports in the local Design Studio after `bun run studio:refresh`; do not maintain an export list here.
24+
The current public exports, variants, defaults, styling slots, and relationships are derived directly from source by the design check and full scanner. Run `bun run check:design --base origin/staging --working-tree` when EMCN or `globals.css` changes. Browse and interact with visual exports in the local Design Studio after `bun run studio:refresh`; do not maintain an export list here.
2525

2626
Use chip fields and triggers for product forms, and `DropdownMenu` for context and action lists. Prefer a supported prop to a `className` chrome override. A selected chip uses `active`; outer spacing belongs to its parent. A view-only value stays at full opacity and uses the read-only copy field or `ChipModalField type='copy'`, not a disabled input. `ChipModalField` owns its inner control chrome; use `type='custom'` with a title for a control it does not cover. A chip dropdown owns its chevron and supports single or multi-select through `multiple`; use chip select or combobox for searchable or grouped lists. Inline tags are labels, not pill triggers.
2727

@@ -49,6 +49,6 @@ Declare keyboard intent on the action-owning primitive; never add document-level
4949

5050
Color tokens and icon-size conventions are canonical in `.claude/rules/sim-styling.md` — follow it rather than restating.
5151

52-
## Generated design contracts
52+
## Source-derived design contracts
5353

54-
Run `bun run design:generate` after public API, styling, recipe or ownership changes and commit `scripts/design-conformance/contracts.generated.json` with the source. CI checks freshness through `check:design-generated`. Ownership is derived from implementation; intentional customization belongs in component TSDoc (`@designAllow <slot> <CSS properties or policy groups>`, `@designProtect` for ownership that cannot be inferred). Review the diff findings after generation; they retain originating central changes. Browser reports and captures remain local, outside the repository.
54+
The diff check derives public API, styling, recipe and ownership facts from both source revisions; the full scan publishes current facts in `scan.json`. Ownership is derived from implementation; intentional customization belongs in component TSDoc (`@designAllow <slot> <CSS properties or policy groups>`, `@designProtect` for ownership that cannot be inferred). Review central source changes in the diff findings. Browser reports and captures remain local, outside the repository.

‎.cursor/rules/emcn-components.mdc‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ The menu surface intentionally diverges from the pill: `dropdown-menu.tsx` items
2222

2323
## Public components and design behavior
2424

25-
The current public exports, variants, defaults, styling slots, and relationships are generated from source in `scripts/design-conformance/contracts.generated.json`. Run `bun run design:generate` when EMCN or `globals.css` changes. Browse and interact with the visual exports in the local Design Studio after `bun run studio:refresh`; do not maintain an export list here.
25+
The current public exports, variants, defaults, styling slots, and relationships are derived directly from source by the design check and full scanner. Run `bun run check:design --base origin/staging --working-tree` when EMCN or `globals.css` changes. Browse and interact with visual exports in the local Design Studio after `bun run studio:refresh`; do not maintain an export list here.
2626

2727
Use chip fields and triggers for product forms, and `DropdownMenu` for context and action lists. Prefer a supported prop to a `className` chrome override. A selected chip uses `active`; outer spacing belongs to its parent. A view-only value stays at full opacity and uses the read-only copy field or `ChipModalField type='copy'`, not a disabled input. `ChipModalField` owns its inner control chrome; use `type='custom'` with a title for a control it does not cover. A chip dropdown owns its chevron and supports single or multi-select through `multiple`; use chip select or combobox for searchable or grouped lists. Inline tags are labels, not pill triggers.
2828

@@ -50,6 +50,6 @@ Declare keyboard intent on the action-owning primitive; never add document-level
5050

5151
Color tokens and icon-size conventions are canonical in `.claude/rules/sim-styling.md` — follow it rather than restating.
5252

53-
## Generated design contracts
53+
## Source-derived design contracts
5454

55-
Run `bun run design:generate` after public API, styling, recipe or ownership changes and commit `scripts/design-conformance/contracts.generated.json` with the source. CI checks freshness through `check:design-generated`. Ownership is derived from implementation; intentional customization belongs in component TSDoc (`@designAllow <slot> <CSS properties or policy groups>`, `@designProtect` for ownership that cannot be inferred). Review the diff findings after generation; they retain originating central changes. Browser reports and captures remain local, outside the repository.
55+
The diff check derives public API, styling, recipe and ownership facts from both source revisions; the full scan publishes current facts in `scan.json`. Ownership is derived from implementation; intentional customization belongs in component TSDoc (`@designAllow <slot> <CSS properties or policy groups>`, `@designProtect` for ownership that cannot be inferred). Review central source changes in the diff findings. Browser reports and captures remain local, outside the repository.

‎CLAUDE.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ The `'use client'` server boundary, the app/worker runtime env split, and featur
106106

107107
## Styling and EMCN
108108

109-
- Tailwind only. Inline `style` only for a genuinely dynamic value or a CSS variable. Keep component-specific styling local. Change tokens in `globals.css` deliberately when the shared design decision changes, then run `bun run design:generate`. `cn()` from `@sim/emcn` for conditional classes. `size-*` for equal height and width (icons default `size-[14px]`), never `h-N w-N`.
109+
- Tailwind only. Inline `style` only for a genuinely dynamic value or a CSS variable. Keep component-specific styling local. Change tokens in `globals.css` deliberately when the shared design decision changes, then run `bun run check:design --base origin/staging --working-tree`. `cn()` from `@sim/emcn` for conditional classes. `size-*` for equal height and width (icons default `size-[14px]`), never `h-N w-N`.
110110
- Import components, `cn`, and tokens from the `@sim/emcn` barrel; icons from `@sim/emcn/icons`; CSS modules by file path. Never deep-import other component subpaths.
111111
- The chip family is the canonical chrome: `ChipInput`, `ChipTextarea`, `ChipModal`/`ChipModalField`, `ChipSelect`/`ChipCombobox`/`ChipDropdown`, `ChipSwitch`, `ChipDatePicker`, `Chip`/`ChipLink`, `ChipTag`; `DropdownMenu` for context/action menus. Components own their chrome: consumers pass props (`error`, `icon`, `endAdornment`, `inputClassName`) and `className` carries only layout/sizing. Every labeled field inside a `ChipModalBody` is a `ChipModalField`.
112112
- Consumer rules, tokens, text scale, and modal rhythm: `.claude/rules/sim-styling.md`. Authoring components in `packages/emcn`: `.claude/rules/emcn-components.md`. Product UI copy: `.claude/rules/sim-ui-copy.md`. Marketing copy and positioning: `.claude/rules/constitution.md`.

‎biome.json‎

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -167,10 +167,7 @@
167167
}
168168
},
169169
{
170-
"includes": [
171-
"scripts/design-conformance/contracts.json",
172-
"scripts/design-conformance/contracts.generated.json"
173-
],
170+
"includes": ["scripts/design-conformance/contracts.json"],
174171
"formatter": {
175172
"enabled": false
176173
}

‎package.json‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -130,9 +130,7 @@
130130
"studio:dev": "next dev tools/design-studio --hostname 127.0.0.1 --port 3001",
131131
"type-check:studio": "tsc --noEmit -p tools/design-studio/tsconfig.json",
132132
"type-check:design": "tsc --noEmit -p scripts/design-conformance/tsconfig.json",
133-
"check:agent-cli-boundary": "bun run scripts/check-agent-cli-boundary.ts",
134-
"design:generate": "bun --no-env-file scripts/generate-design-contracts.ts",
135-
"check:design-generated": "bun run --no-env-file scripts/generate-design-contracts.ts --check"
133+
"check:agent-cli-boundary": "bun run scripts/check-agent-cli-boundary.ts"
136134
},
137135
"overrides": {
138136
"react": "19.2.4",

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

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,21 @@ import { ciRefs, warningExitCode } from '#design-conformance/ci'
88
import { repositoryRoot } from '#design-conformance/command'
99
import { ConformanceLinter } from '#design-conformance/conformance'
1010
import { contractsHash, isRegistry } from '#design-conformance/contracts'
11+
import { generateContracts } from '#design-conformance/generated-contracts'
1112
import { compareGit, git } from '#design-conformance/io'
12-
import { hash, type Report, TOKEN_FILE } from '#design-conformance/model'
13+
import { hash, inspectionFailure, type Report, TOKEN_FILE } from '#design-conformance/model'
1314
import {
1415
escapeAnnotation,
1516
githubAnnotations,
1617
githubSummary,
1718
textReport,
1819
} from '#design-conformance/reporting'
1920
import { snapshotHash } from '#design-conformance/system-snapshot'
21+
import { GitSource } from '#design-conformance/worktree-source'
2022

2123
const temp = mkdtempSync(path.join(os.tmpdir(), 'design-command-'))
2224
afterAll(() => rmSync(temp, { recursive: true, force: true }))
2325
const cli = fileURLToPath(new URL('./check-design-conformance.ts', import.meta.url))
24-
const generatorCli = fileURLToPath(new URL('./generate-design-contracts.ts', import.meta.url))
2526
const ci = fileURLToPath(new URL('./design-conformance/ci.ts', import.meta.url))
2627
const ui = 'apps/sim/components/example.tsx'
2728

@@ -280,8 +281,8 @@ test.each([
280281
['truthy string value', "{'bg-red-500':'text-[37px]'}", ['background-color']],
281282
['dynamic value', "{'p-2':active}", ['padding']],
282283
] as const)(
283-
'source metadata follows class map enablement through the generator CLI: %s',
284-
(_name, map, protectedProperties) => {
284+
'source metadata follows class map enablement through source analysis: %s',
285+
async (_name, map, protectedProperties) => {
285286
const { repo } = fixture()
286287
const write = (file: string, source: string) => {
287288
mkdirSync(path.dirname(path.join(repo, file)), { recursive: true })
@@ -298,11 +299,7 @@ export function Example({className,...props}:HTMLAttributes<HTMLDivElement>){ret
298299
throw new Error('Product source must not execute')
299300
`
300301
)
301-
const result = run(['--repo', repo], generatorCli)
302-
expect(result.status, result.stderr).toBe(0)
303-
const metadata = JSON.parse(
304-
readFileSync(path.join(repo, 'scripts/design-conformance/contracts.generated.json'), 'utf8')
305-
)
302+
const metadata = await generateContracts(new GitSource(repo, 'HEAD', true).central())
306303
expect(metadata.exports.Example.slots.className.protected).toEqual(protectedProperties)
307304
}
308305
)
@@ -1055,6 +1052,7 @@ test('unchecked diagnostics preserve both sides and safely render source-authore
10551052
side,
10561053
context: 'className',
10571054
reason: 'Unknown helper: <script>& value\r\n::warning::injected',
1055+
relevant: true,
10581056
})
10591057
const original = JSON.stringify(report)
10601058
const text = textReport(report)
@@ -1070,23 +1068,45 @@ test('unchecked diagnostics preserve both sides and safely render source-authore
10701068
expect(JSON.stringify(report)).toBe(original)
10711069
})
10721070

1073-
test('large unchecked summaries show explicit limits while logs retain every complete diagnostic', () => {
1071+
test('large finding messages stay bounded in logs while JSON keeps full evidence', () => {
1072+
const report = findingReport()
1073+
report.findings[0].kind = 'system-change'
1074+
report.findings[0].before = 'a'.repeat(3000)
1075+
const log = textReport(report)
1076+
const annotation = githubAnnotations(report)[0]
1077+
expect(log).toContain('[truncated; see JSON]')
1078+
expect(log).not.toContain('a'.repeat(3000))
1079+
expect(annotation).toContain('[truncated; see JSON]')
1080+
expect(annotation).not.toContain('a'.repeat(3000))
1081+
expect(JSON.stringify(report)).toContain('a'.repeat(3000))
1082+
})
1083+
1084+
test('large unchecked summaries keep complete JSON evidence and bound human output', () => {
10741085
const report = new ConformanceLinter().report(null)
10751086
report.unchecked = Array.from({ length: 101 }, (_, index) => ({
10761087
file: `component-${index}.tsx`,
10771088
line: 1,
10781089
side: 'after',
10791090
context: '',
10801091
reason: index === 0 ? `${'&'.repeat(2000)} complete-long-diagnostic` : `reason-${index}`,
1092+
relevant: true,
10811093
}))
10821094
const markdown = githubSummary(report)
10831095
const text = textReport(report)
1084-
expect(markdown).toContain('Showing 100 of 101 diagnostics')
1085-
expect(markdown).toContain('[truncated; see check log]')
1096+
expect(markdown).toContain('Showing 20 of 101 diagnostics')
1097+
expect(markdown).toContain('[truncated; see JSON]')
10861098
expect(markdown).not.toContain('complete-long-diagnostic')
10871099
expect(markdown).not.toContain('component-100.tsx')
1088-
expect(text).toContain('complete-long-diagnostic')
1089-
expect(text).toContain('component-100.tsx:1 (after) — reason-100')
1100+
expect(text).toContain('101 unchecked diagnostics')
1101+
expect(text).not.toContain('component-100.tsx:1 (after) — reason-100')
1102+
expect(JSON.stringify(report)).toContain('component-100.tsx')
1103+
})
1104+
1105+
test('inspection severity is explicit and does not depend on diagnostic wording', () => {
1106+
expect(inspectionFailure({ reason: 'Arbitrary prose', inspection: 'failed' })).toBe(true)
1107+
expect(inspectionFailure({ reason: 'Parser failure in a supported but unresolved flow' })).toBe(
1108+
false
1109+
)
10901110
})
10911111

10921112
test('system edits remain flagged but are reported as design review warnings', () => {

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

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,16 @@
11
/** biome-ignore-all lint/suspicious/noTemplateCurlyInString: Fixtures contain proposed source text. */
2-
import { readFileSync } from 'node:fs'
32
import { expect, test } from 'vitest'
43
import { inspectControlAnalysis } from '#control-analysis/analysis'
54
import type { ControlSource } from '#control-analysis/model'
65
import { ReviewCollector } from '#control-analysis/review'
76
import { productScope } from '#control-analysis/scope'
87
import { extract } from '#design-conformance/extract'
9-
import type { GeneratedContracts } from '#design-conformance/generated-contracts'
10-
import { inspectionFailure } from '#design-conformance/model'
8+
import { generateContracts } from '#design-conformance/generated-contracts'
9+
import { GitSource } from '#design-conformance/worktree-source'
1110

1211
const globals = 'apps/sim/app/_styles/globals.css'
1312
const ui = 'apps/sim/components/example.tsx'
14-
const metadata = JSON.parse(
15-
readFileSync('scripts/design-conformance/contracts.generated.json', 'utf8')
16-
) as GeneratedContracts
13+
const metadata = await generateContracts(new GitSource(process.cwd(), 'HEAD', true).central())
1714
function source(files: Record<string, string>): ControlSource {
1815
return {
1916
entries: Object.entries(files).map(([path, text]) => ({
@@ -34,7 +31,6 @@ const inspect = (files: Record<string, string>) =>
3431
}),
3532
'forward',
3633
undefined,
37-
undefined,
3834
metadata
3935
)
4036

@@ -49,6 +45,7 @@ test('browser desktop UI is checked while landing, docs, native desktop and API
4945
'apps/sim/app/design-studio/components/page.tsx',
5046
'apps/desktop/src/main.tsx',
5147
'apps/sim/lib/desktop/appearance.ts',
48+
'apps/sim/scripts/example.tsx',
5249
'apps/sim/tools/generated/tool-metadata.ts',
5350
'apps/sim/app/api/desktop/auth/route.ts',
5451
])
@@ -400,15 +397,6 @@ test.each([
400397
)
401398
})
402399

403-
test.each([
404-
'CSS assignment source is nonregular or exceeds the parsing limit',
405-
'Artwork parser failure; only blob change is known',
406-
'Artwork source exceeds the 2 MiB parsing limit',
407-
'Artwork symlink/submodule is not followed',
408-
])('inspection failures are not unresolved styling: %s', (reason) => {
409-
expect(inspectionFailure(reason)).toBe(true)
410-
})
411-
412400
test.each(['button', 'input', 'textarea', 'select', 'section > button.action', 'button + input'])(
413401
'bare native CSS controls are reviewable: %s',
414402
(selector) => {

0 commit comments

Comments
 (0)