openuispec 0.2.16 → 0.2.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/check/audit.ts +183 -8
- package/docs/cli.md +9 -4
- package/docs/implementation-notes.md +1 -1
- package/examples/social-app/openuispec/openuispec.yaml +10 -0
- package/examples/taskflow/openuispec/openuispec.yaml +9 -0
- package/examples/todo-orbit/openuispec/openuispec.yaml +11 -0
- package/package.json +1 -1
- package/prepare/index.ts +64 -7
- package/schema/openuispec.schema.json +8 -0
- package/schema/validate.ts +21 -22
- package/spec/openuispec-v0.2.md +159 -13
package/README.md
CHANGED
|
@@ -54,8 +54,9 @@ This scaffolds a spec directory, starter tokens, and **configures the MCP server
|
|
|
54
54
|
- **Data binding** — reactive state, format expressions, caching, and loading/error/empty states
|
|
55
55
|
- **Adaptive layout** — size classes (compact/regular/expanded) with per-section overrides
|
|
56
56
|
- **Platform adaptation** — per-target overrides for iOS, Android, and Web behaviors
|
|
57
|
-
- **Design intent** — `design` section
|
|
58
|
-
- **Anti-patterns** — `must_avoid` in contracts and `
|
|
57
|
+
- **Design intent** — `design` section captures brand personality, complexity level (`restrained`/`balanced`/`elaborate`), quality tier (`mvp`/`production`/`flagship`), and audience — generators match visual elaborateness and polish level accordingly
|
|
58
|
+
- **Anti-patterns** — `must_avoid` in contracts and `universal_anti_patterns` in the manifest (9 domains: typography, color, spacing, motion, elevation, layout, visual, interaction, accessibility) steer AI away from generic design mistakes. Platform-scoped with `[web]`/`[ios]`/`[android]` tags
|
|
59
|
+
- **Design quality audit** — `check --audit` scores the spec against 18 heuristic checks across all token and contract domains, producing a numeric score with CI-gatable thresholds
|
|
59
60
|
|
|
60
61
|
## The 7 contract families
|
|
61
62
|
|
|
@@ -101,7 +102,7 @@ Screenshots of the generated apps are in the [artifacts](./artifacts/) directory
|
|
|
101
102
|
|-----|-------------|
|
|
102
103
|
| [CLI & MCP Tools](./docs/cli.md) | All CLI commands, MCP tools, screenshot params, target workflow |
|
|
103
104
|
| [File Formats & Schemas](./docs/file-formats.md) | File types, JSON schemas, output directories, spec sections |
|
|
104
|
-
| [Full Specification](./spec/openuispec-v0.2.md) | Complete v0.2 spec (
|
|
105
|
+
| [Full Specification](./spec/openuispec-v0.2.md) | Complete v0.2 spec (16 sections) |
|
|
105
106
|
| [llms-full.txt](https://openuispec.rsteam.uz/llms-full.txt) | Spec + all schemas in one file (for AI consumption) |
|
|
106
107
|
|
|
107
108
|
## Status
|
package/check/audit.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* openuispec check --target web --audit --format json
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { readFileSync } from "node:fs";
|
|
13
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
14
14
|
import { join, resolve } from "node:path";
|
|
15
15
|
import YAML from "yaml";
|
|
16
16
|
|
|
@@ -31,6 +31,16 @@ export interface AuditResult {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
const AI_DEFAULT_FONTS = new Set(["Inter", "Roboto", "Arial", "Open Sans"]);
|
|
34
|
+
const REQUIRED_TOKEN_FILES = [
|
|
35
|
+
"color.yaml",
|
|
36
|
+
"typography.yaml",
|
|
37
|
+
"spacing.yaml",
|
|
38
|
+
"elevation.yaml",
|
|
39
|
+
"motion.yaml",
|
|
40
|
+
"layout.yaml",
|
|
41
|
+
"themes.yaml",
|
|
42
|
+
"icons.yaml",
|
|
43
|
+
];
|
|
34
44
|
|
|
35
45
|
function readYaml(path: string): any {
|
|
36
46
|
try {
|
|
@@ -40,8 +50,36 @@ function readYaml(path: string): any {
|
|
|
40
50
|
}
|
|
41
51
|
}
|
|
42
52
|
|
|
53
|
+
function readYamlForAudit(path: string, domain: string, findings: AuditFinding[]): any {
|
|
54
|
+
try {
|
|
55
|
+
return YAML.parse(readFileSync(path, "utf-8"));
|
|
56
|
+
} catch (err: any) {
|
|
57
|
+
if (err?.code === "ENOENT") return null;
|
|
58
|
+
findings.push({
|
|
59
|
+
domain,
|
|
60
|
+
rule: "unreadable_file",
|
|
61
|
+
severity: "error",
|
|
62
|
+
message: `Could not parse "${path}". Fix malformed YAML before relying on this audit.`,
|
|
63
|
+
});
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function checkRequiredTokenFiles(tokensDir: string, findings: AuditFinding[]): void {
|
|
69
|
+
for (const filename of REQUIRED_TOKEN_FILES) {
|
|
70
|
+
if (!existsSync(join(tokensDir, filename))) {
|
|
71
|
+
findings.push({
|
|
72
|
+
domain: "tokens",
|
|
73
|
+
rule: "missing_file",
|
|
74
|
+
severity: "error",
|
|
75
|
+
message: `Required token file "${filename}" is missing.`,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
43
81
|
function checkTypography(tokensDir: string, findings: AuditFinding[]): void {
|
|
44
|
-
const doc =
|
|
82
|
+
const doc = readYamlForAudit(join(tokensDir, "typography.yaml"), "typography", findings);
|
|
45
83
|
if (!doc?.typography) return;
|
|
46
84
|
|
|
47
85
|
// Font diversity: primary must NOT be a common AI default
|
|
@@ -65,10 +103,28 @@ function checkTypography(tokensDir: string, findings: AuditFinding[]): void {
|
|
|
65
103
|
message: `Only ${scaleKeys.length} type scale level(s) defined. Use ≥4 distinct levels for clear hierarchy.`,
|
|
66
104
|
});
|
|
67
105
|
}
|
|
106
|
+
|
|
107
|
+
// Weight hierarchy: at least 2 distinct weights
|
|
108
|
+
if (doc.typography.scale) {
|
|
109
|
+
const weights = new Set<number>();
|
|
110
|
+
for (const level of Object.values(doc.typography.scale)) {
|
|
111
|
+
if (typeof (level as any)?.weight === "number") {
|
|
112
|
+
weights.add((level as any).weight);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (weights.size > 0 && weights.size < 2) {
|
|
116
|
+
findings.push({
|
|
117
|
+
domain: "typography",
|
|
118
|
+
rule: "weight_hierarchy",
|
|
119
|
+
severity: "warning",
|
|
120
|
+
message: "Only 1 distinct font weight used across the type scale. Use ≥2 weights (e.g. 400 + 700) for clear hierarchy.",
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
68
124
|
}
|
|
69
125
|
|
|
70
126
|
function checkColor(tokensDir: string, findings: AuditFinding[]): void {
|
|
71
|
-
const doc =
|
|
127
|
+
const doc = readYamlForAudit(join(tokensDir, "color.yaml"), "color", findings);
|
|
72
128
|
if (!doc?.color) return;
|
|
73
129
|
|
|
74
130
|
// Pure black/white check
|
|
@@ -101,10 +157,27 @@ function checkColor(tokensDir: string, findings: AuditFinding[]): void {
|
|
|
101
157
|
}
|
|
102
158
|
scanForPure(doc.color, "color");
|
|
103
159
|
|
|
160
|
+
// Semantic color completeness: success, warning, danger, info
|
|
161
|
+
if (doc.color.semantic) {
|
|
162
|
+
const required = ["success", "warning", "danger", "info"];
|
|
163
|
+
const defined = Object.keys(doc.color.semantic);
|
|
164
|
+
for (const name of required) {
|
|
165
|
+
if (!defined.includes(name)) {
|
|
166
|
+
findings.push({
|
|
167
|
+
domain: "color",
|
|
168
|
+
rule: "semantic_completeness",
|
|
169
|
+
severity: "warning",
|
|
170
|
+
message: `Semantic color "${name}" is missing. Define all four (success, warning, danger, info) for complete state coverage.`,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
104
176
|
// Theme coverage: check themes.yaml for both light + dark
|
|
105
177
|
{
|
|
106
|
-
const themes =
|
|
107
|
-
|
|
178
|
+
const themes = readYamlForAudit(join(tokensDir, "themes.yaml"), "color", findings);
|
|
179
|
+
if (!themes?.themes) return;
|
|
180
|
+
const themeKeys = Object.keys(themes.themes);
|
|
108
181
|
const hasLight = themeKeys.some((k) => k.includes("light"));
|
|
109
182
|
const hasDark = themeKeys.some((k) => k.includes("dark"));
|
|
110
183
|
if (!hasLight || !hasDark) {
|
|
@@ -119,7 +192,7 @@ function checkColor(tokensDir: string, findings: AuditFinding[]): void {
|
|
|
119
192
|
}
|
|
120
193
|
|
|
121
194
|
function checkSpacing(tokensDir: string, findings: AuditFinding[]): void {
|
|
122
|
-
const doc =
|
|
195
|
+
const doc = readYamlForAudit(join(tokensDir, "spacing.yaml"), "spacing", findings);
|
|
123
196
|
if (!doc?.spacing) return;
|
|
124
197
|
|
|
125
198
|
// Scale usage: at least 4 distinct values
|
|
@@ -156,7 +229,7 @@ function checkSpacing(tokensDir: string, findings: AuditFinding[]): void {
|
|
|
156
229
|
}
|
|
157
230
|
|
|
158
231
|
function checkMotion(tokensDir: string, findings: AuditFinding[]): void {
|
|
159
|
-
const doc =
|
|
232
|
+
const doc = readYamlForAudit(join(tokensDir, "motion.yaml"), "motion", findings);
|
|
160
233
|
if (!doc?.motion) return;
|
|
161
234
|
|
|
162
235
|
// Duration variety: at least 2 distinct durations
|
|
@@ -180,12 +253,110 @@ function checkMotion(tokensDir: string, findings: AuditFinding[]): void {
|
|
|
180
253
|
message: "motion.reduced_motion is not defined. Must specify policy for prefers-reduced-motion.",
|
|
181
254
|
});
|
|
182
255
|
}
|
|
256
|
+
|
|
257
|
+
// Easing quality: enter + exit curves, at least one cubic-bezier
|
|
258
|
+
if (doc.motion.easing) {
|
|
259
|
+
const easings = doc.motion.easing;
|
|
260
|
+
const keys = Object.keys(easings);
|
|
261
|
+
if (!keys.includes("enter") || !keys.includes("exit")) {
|
|
262
|
+
findings.push({
|
|
263
|
+
domain: "motion",
|
|
264
|
+
rule: "easing_quality",
|
|
265
|
+
severity: "warning",
|
|
266
|
+
message: "Motion easing should define at least 'enter' and 'exit' curves for asymmetric transitions.",
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
const hasCubicBezier = Object.values(easings).some(
|
|
270
|
+
(v) => typeof v === "string" && v.includes("cubic-bezier"),
|
|
271
|
+
);
|
|
272
|
+
if (!hasCubicBezier) {
|
|
273
|
+
findings.push({
|
|
274
|
+
domain: "motion",
|
|
275
|
+
rule: "easing_quality",
|
|
276
|
+
severity: "warning",
|
|
277
|
+
message: "All easing curves are generic keywords. Use at least one cubic-bezier() for nuanced motion.",
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function checkElevationProgression(tokensDir: string, findings: AuditFinding[]): void {
|
|
284
|
+
const doc = readYamlForAudit(join(tokensDir, "elevation.yaml"), "elevation", findings);
|
|
285
|
+
if (!doc?.elevation) return;
|
|
286
|
+
const levels = Object.keys(doc.elevation).filter((k) => k !== "none");
|
|
287
|
+
if (levels.length < 2) {
|
|
288
|
+
findings.push({
|
|
289
|
+
domain: "elevation",
|
|
290
|
+
rule: "level_count",
|
|
291
|
+
severity: "warning",
|
|
292
|
+
message: `Only ${levels.length} non-none elevation level(s) defined. Define ≥2 (e.g. sm, md, lg) for meaningful depth hierarchy.`,
|
|
293
|
+
});
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
const androidValues: number[] = [];
|
|
297
|
+
for (const level of levels) {
|
|
298
|
+
const val = doc.elevation[level]?.platform?.android?.elevation;
|
|
299
|
+
if (typeof val === "number") androidValues.push(val);
|
|
300
|
+
}
|
|
301
|
+
if (androidValues.length >= 2) {
|
|
302
|
+
for (let i = 1; i < androidValues.length; i++) {
|
|
303
|
+
if (androidValues[i] <= androidValues[i - 1]) {
|
|
304
|
+
findings.push({
|
|
305
|
+
domain: "elevation",
|
|
306
|
+
rule: "progression",
|
|
307
|
+
severity: "warning",
|
|
308
|
+
message: "Elevation levels do not increase monotonically. Each level should cast a deeper shadow than the previous.",
|
|
309
|
+
});
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function checkLayoutSizeClasses(tokensDir: string, findings: AuditFinding[]): void {
|
|
317
|
+
const doc = readYamlForAudit(join(tokensDir, "layout.yaml"), "layout", findings);
|
|
318
|
+
if (!doc?.layout?.size_classes) return;
|
|
319
|
+
const classes = Object.keys(doc.layout.size_classes);
|
|
320
|
+
if (classes.length < 2) {
|
|
321
|
+
findings.push({
|
|
322
|
+
domain: "layout",
|
|
323
|
+
rule: "size_class_coverage",
|
|
324
|
+
severity: "warning",
|
|
325
|
+
message: `Only ${classes.length} size class(es) defined. Define at least compact + regular for responsive layouts.`,
|
|
326
|
+
});
|
|
327
|
+
} else if (!classes.includes("compact")) {
|
|
328
|
+
findings.push({
|
|
329
|
+
domain: "layout",
|
|
330
|
+
rule: "size_class_coverage",
|
|
331
|
+
severity: "warning",
|
|
332
|
+
message: "No 'compact' size class defined. Mobile-first layouts require a compact breakpoint.",
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function checkContractStateCoverage(contractsDir: string, findings: AuditFinding[]): void {
|
|
338
|
+
if (!existsSync(contractsDir)) return;
|
|
339
|
+
for (const file of readdirSync(contractsDir).filter((f) => f.endsWith(".yaml") && !f.startsWith("x_"))) {
|
|
340
|
+
const doc = readYamlForAudit(join(contractsDir, file), "contracts", findings);
|
|
341
|
+
if (!doc) continue;
|
|
342
|
+
const contractName = Object.keys(doc)[0];
|
|
343
|
+
const contract = doc[contractName];
|
|
344
|
+
const mustHandle: string[] = contract?.generation?.must_handle ?? [];
|
|
345
|
+
if (mustHandle.length === 0) {
|
|
346
|
+
findings.push({
|
|
347
|
+
domain: "contracts",
|
|
348
|
+
rule: "state_coverage",
|
|
349
|
+
severity: "warning",
|
|
350
|
+
message: `Contract "${contractName}" has no generation.must_handle entries. Define required states for AI compliance.`,
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
}
|
|
183
354
|
}
|
|
184
355
|
|
|
185
356
|
function checkContracts(contractsDir: string, findings: AuditFinding[]): void {
|
|
186
357
|
// All collections have empty_state in must_handle or variants
|
|
187
358
|
{
|
|
188
|
-
const doc =
|
|
359
|
+
const doc = readYamlForAudit(join(contractsDir, "collection.yaml"), "contracts", findings);
|
|
189
360
|
const collection = doc ? doc[Object.keys(doc)[0]] : null;
|
|
190
361
|
if (collection) {
|
|
191
362
|
const mustHandle: string[] = collection.generation?.must_handle ?? [];
|
|
@@ -211,11 +382,15 @@ export function buildAuditResult(projectDir: string, threshold: number = 0): Aud
|
|
|
211
382
|
const effectiveThreshold = threshold > 0 ? threshold : (manifest?.generation_guidance?.audit_threshold ?? 0);
|
|
212
383
|
|
|
213
384
|
const findings: AuditFinding[] = [];
|
|
385
|
+
checkRequiredTokenFiles(tokensDir, findings);
|
|
214
386
|
checkTypography(tokensDir, findings);
|
|
215
387
|
checkColor(tokensDir, findings);
|
|
216
388
|
checkSpacing(tokensDir, findings);
|
|
217
389
|
checkMotion(tokensDir, findings);
|
|
390
|
+
checkElevationProgression(tokensDir, findings);
|
|
391
|
+
checkLayoutSizeClasses(tokensDir, findings);
|
|
218
392
|
checkContracts(contractsDir, findings);
|
|
393
|
+
checkContractStateCoverage(contractsDir, findings);
|
|
219
394
|
|
|
220
395
|
const errors = findings.filter((f) => f.severity === "error").length;
|
|
221
396
|
const warnings = findings.filter((f) => f.severity === "warning").length;
|
package/docs/cli.md
CHANGED
|
@@ -289,10 +289,15 @@ openuispec check --target web --audit --json # machine-readable output
|
|
|
289
289
|
|
|
290
290
|
| Domain | What's checked |
|
|
291
291
|
|--------|---------------|
|
|
292
|
-
|
|
|
293
|
-
|
|
|
292
|
+
| Tokens | All 8 required token files exist |
|
|
293
|
+
| Typography | Primary font not an AI default · ≥4 scale levels · ≥2 distinct weights |
|
|
294
|
+
| Color | No pure #000000/#FFFFFF · success/warning/danger/info semantic colors · light + dark themes |
|
|
294
295
|
| Spacing | ≥4 scale values · `page_margin` and `card_padding` aliases present |
|
|
295
|
-
| Motion | ≥2 distinct durations · `reduced_motion` policy
|
|
296
|
-
|
|
|
296
|
+
| Motion | ≥2 distinct durations · `reduced_motion` policy · enter/exit easing · ≥1 cubic-bezier curve |
|
|
297
|
+
| Elevation | ≥2 non-none levels · monotonically increasing progression |
|
|
298
|
+
| Layout | ≥2 size classes · compact class defined |
|
|
299
|
+
| Contracts | `collection` has `empty_state` in `must_handle` · all contracts have non-empty `must_handle` |
|
|
297
300
|
|
|
298
301
|
The `audit_threshold` in `generation_guidance` sets the project-wide minimum score. `--min-score` overrides it per-run.
|
|
302
|
+
|
|
303
|
+
See [Section 16 of the spec](../spec/openuispec-v0.2.md#16-design-intent-and-generation-guidance) for complete documentation of design intent, anti-patterns, and quality tiers.
|
|
@@ -166,6 +166,6 @@
|
|
|
166
166
|
|
|
167
167
|
`openuispec check --audit` runs design quality heuristics against token and contract files, returning a numeric score (`max(0, 100 - errors × 10 - warnings × 3)`) and categorized findings. The `audit_threshold` in `generation_guidance` sets a project-wide minimum; `--min-score N` overrides per-run.
|
|
168
168
|
|
|
169
|
-
`openuispec prepare` includes `anti_patterns` (universal + contract-specific + project-specific, filtered by target platform) and `design_context` (personality, complexity, audience, complexity_rule) in its output when the manifest defines `generation_guidance` and `design` sections.
|
|
169
|
+
`openuispec prepare` includes `anti_patterns` (universal + contract-specific + project-specific, filtered by target platform) and `design_context` (personality, complexity, quality_tier, audience, complexity_rule, quality_tier_rule, quality_test) in its output when the manifest defines `generation_guidance` and `design` sections. The `quality_test` field is an auto-generated AI-slop checklist tuned to the project's complexity and quality tier.
|
|
170
170
|
|
|
171
171
|
`generation.extra_rules` in the manifest is included in prepare output and filtered by platform tag.
|
|
@@ -57,6 +57,15 @@ generation_guidance:
|
|
|
57
57
|
layout:
|
|
58
58
|
- "Do not assume large-screen layouts work on compact — every multi-pane pattern needs an explicit compact fallback"
|
|
59
59
|
- "Do not use pixel breakpoints — reference size classes by name (compact, regular, expanded)"
|
|
60
|
+
visual:
|
|
61
|
+
- "Do not flatten depth — use elevation tokens to separate layers and give content visual weight"
|
|
62
|
+
- "Do not use generic drop shadows — map every shadow to a specific elevation level from tokens"
|
|
63
|
+
- "Do not apply glassmorphism to interactive controls — reserve frosted effects for non-interactive overlays only"
|
|
64
|
+
interaction:
|
|
65
|
+
- "Do not make swipe gestures the only way to access actions — provide a visible alternative"
|
|
66
|
+
- "Do not delay visual feedback on tap — social content requires instant response"
|
|
67
|
+
- "[web] Do not use :focus for focus rings — use :focus-visible to show rings only for keyboard navigation"
|
|
68
|
+
- "[ios] Do not replace native swipe-back with custom gestures — users expect system navigation"
|
|
60
69
|
accessibility:
|
|
61
70
|
- "Do not use color as the only differentiator between states — combine with icon, border, or text changes"
|
|
62
71
|
- "Do not skip focus ring styles — keyboard users must see where focus is at all times"
|
|
@@ -66,6 +75,7 @@ generation_guidance:
|
|
|
66
75
|
design:
|
|
67
76
|
personality: "Vibrant, social, content-rich — engagement and discovery are primary, utility is secondary"
|
|
68
77
|
complexity: "elaborate"
|
|
78
|
+
quality_tier: "flagship"
|
|
69
79
|
audience: "Mobile-first social media users who expect rich media, smooth animations, and expressive interactions"
|
|
70
80
|
avoid:
|
|
71
81
|
- "Do not use muted or desaturated brand colors — the palette should feel energetic"
|
|
@@ -64,6 +64,14 @@ generation_guidance:
|
|
|
64
64
|
layout:
|
|
65
65
|
- "Do not assume large-screen layouts work on compact — every multi-pane pattern needs an explicit compact fallback"
|
|
66
66
|
- "Do not use pixel breakpoints — reference size classes by name (compact, regular, expanded)"
|
|
67
|
+
visual:
|
|
68
|
+
- "Do not use glassmorphism or frosted-glass effects — this is a professional productivity tool"
|
|
69
|
+
- "Do not apply gradient text — it fails contrast checks and looks dated"
|
|
70
|
+
- "[web] Do not use box-shadow on every container — reserve elevation for interactive or raised elements"
|
|
71
|
+
interaction:
|
|
72
|
+
- "Do not treat hover and focus as the same state — keyboard users never see hover"
|
|
73
|
+
- "Do not use confirmation dialogs for reversible actions — use undo with a toast instead"
|
|
74
|
+
- "[web] Do not use :focus for focus rings — use :focus-visible to show rings only for keyboard navigation"
|
|
67
75
|
accessibility:
|
|
68
76
|
- "Do not use color as the only differentiator between states — combine with icon, border, or text changes"
|
|
69
77
|
- "Do not skip focus ring styles — keyboard users must see where focus is at all times"
|
|
@@ -73,6 +81,7 @@ generation_guidance:
|
|
|
73
81
|
design:
|
|
74
82
|
personality: "Clean, focused, productivity-first — no decorative flourishes. Color is used for priority and status clarity, not aesthetics."
|
|
75
83
|
complexity: "balanced"
|
|
84
|
+
quality_tier: "production"
|
|
76
85
|
audience: "Individual contributors and small teams managing personal and shared task lists"
|
|
77
86
|
avoid:
|
|
78
87
|
- "Do not use playful or rounded UI patterns — this is a professional productivity tool"
|
|
@@ -63,6 +63,16 @@ generation_guidance:
|
|
|
63
63
|
layout:
|
|
64
64
|
- "Do not assume large-screen layouts work on compact — every multi-pane pattern needs an explicit compact fallback"
|
|
65
65
|
- "Do not use pixel breakpoints — reference size classes by name (compact, regular, expanded)"
|
|
66
|
+
visual:
|
|
67
|
+
- "Do not mix rounded and sharp corners within the same card or surface — the spec uses cut-corner shapes exclusively"
|
|
68
|
+
- "Do not add decorative gradients, glows, or background textures — this is a restrained design"
|
|
69
|
+
- "Do not apply gradient text — it fails contrast checks and undermines the minimal aesthetic"
|
|
70
|
+
- "[web] Do not use box-shadow on every container — reserve elevation for interactive or raised elements"
|
|
71
|
+
interaction:
|
|
72
|
+
- "Do not treat hover and focus as the same state — keyboard users never see hover"
|
|
73
|
+
- "Do not use long-press as the primary action trigger — use explicit buttons or swipe actions"
|
|
74
|
+
- "Do not delay visual feedback on tap — the press_feedback pattern requires instant response"
|
|
75
|
+
- "[web] Do not use :focus for focus rings — use :focus-visible to show rings only for keyboard navigation"
|
|
66
76
|
accessibility:
|
|
67
77
|
- "Do not use color as the only differentiator between states — combine with icon, border, or text changes"
|
|
68
78
|
- "Do not skip focus ring styles — keyboard users must see where focus is at all times"
|
|
@@ -72,6 +82,7 @@ generation_guidance:
|
|
|
72
82
|
design:
|
|
73
83
|
personality: "Minimal and calm — orbital metaphor suggests clarity and organization without visual noise"
|
|
74
84
|
complexity: "restrained"
|
|
85
|
+
quality_tier: "production"
|
|
75
86
|
audience: "Bilingual individuals who prefer a clean, low-distraction task environment"
|
|
76
87
|
avoid:
|
|
77
88
|
- "Do not add decorative illustrations or background patterns"
|
package/package.json
CHANGED
package/prepare/index.ts
CHANGED
|
@@ -180,8 +180,11 @@ export interface PrepareResult {
|
|
|
180
180
|
design_context?: {
|
|
181
181
|
personality?: string;
|
|
182
182
|
complexity: 'restrained' | 'balanced' | 'elaborate';
|
|
183
|
+
quality_tier: 'mvp' | 'production' | 'flagship';
|
|
183
184
|
audience?: string;
|
|
184
185
|
complexity_rule: string;
|
|
186
|
+
quality_tier_rule: string;
|
|
187
|
+
quality_test: string;
|
|
185
188
|
};
|
|
186
189
|
next_steps: string[];
|
|
187
190
|
}
|
|
@@ -666,8 +669,8 @@ function generationRules(target: string, outputDir: string, manifest: Record<str
|
|
|
666
669
|
}
|
|
667
670
|
|
|
668
671
|
function matchesTargetPlatform(item: string, target: string): boolean {
|
|
669
|
-
const tagMatch = item.match(/^\[([a-z]+)\]/);
|
|
670
|
-
return !tagMatch || tagMatch[1] === target;
|
|
672
|
+
const tagMatch = item.match(/^\[([a-z]+)\]/i);
|
|
673
|
+
return !tagMatch || tagMatch[1].toLowerCase() === target;
|
|
671
674
|
}
|
|
672
675
|
|
|
673
676
|
function complexityRule(complexity: string): string {
|
|
@@ -681,15 +684,63 @@ function complexityRule(complexity: string): string {
|
|
|
681
684
|
}
|
|
682
685
|
}
|
|
683
686
|
|
|
687
|
+
function qualityTierRule(tier: string): string {
|
|
688
|
+
switch (tier) {
|
|
689
|
+
case 'mvp':
|
|
690
|
+
return 'Functional-only. Use semantic tokens but tolerate simple layouts. Skip elevation, motion patterns, and adaptive breakpoints.';
|
|
691
|
+
case 'flagship':
|
|
692
|
+
return 'Pixel-perfect. Every token, motion pattern, elevation level, and adaptive breakpoint must be implemented. All contract states required. No shortcuts.';
|
|
693
|
+
default:
|
|
694
|
+
return 'Production-quality. Apply all tokens, handle accessibility, support adaptive breakpoints. Motion and elevation expected but minor shortcuts acceptable.';
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function buildQualityTest(complexity: string, qualityTier: string, personality?: string): string {
|
|
699
|
+
const items: string[] = [
|
|
700
|
+
'Inter/Roboto/Arial as the primary font when the spec defines a custom font_family.',
|
|
701
|
+
'Pure black (#000000) or pure white (#FFFFFF) — all colors must resolve through tokens.',
|
|
702
|
+
'Cyan-on-dark, purple-to-blue gradient, or neon accent color schemes not in color tokens.',
|
|
703
|
+
'Card-wrapping every content group — cards are for distinct, comparable items only.',
|
|
704
|
+
'Identical spacing values throughout — the spec defines a scale with distinct levels.',
|
|
705
|
+
'Bounce or elastic easing — use only the easing curves from motion tokens.',
|
|
706
|
+
'Shadows on elements with no elevation token assigned.',
|
|
707
|
+
'A single font weight everywhere — the type scale defines multiple weights for hierarchy.',
|
|
708
|
+
];
|
|
709
|
+
|
|
710
|
+
if (complexity === 'restrained') {
|
|
711
|
+
items.push('Any decorative animation, gradient, glassmorphism, or background effect — this is a restrained design.');
|
|
712
|
+
} else if (complexity === 'elaborate') {
|
|
713
|
+
items.push('Missing entrance animations or transition effects — this is an elaborate design that expects rich motion.');
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
if (qualityTier === 'flagship') {
|
|
717
|
+
items.push('Any adaptive breakpoint missing — flagship quality requires all size classes implemented.');
|
|
718
|
+
items.push('Any must_handle state not implemented — flagship requires full contract compliance.');
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
const numbered = items.map((item, i) => `${i + 1}. ${item}`);
|
|
722
|
+
numbered.unshift('After generation, verify the output does NOT exhibit these AI-slop indicators:');
|
|
723
|
+
|
|
724
|
+
if (personality) {
|
|
725
|
+
numbered.push(`Design personality check: "${personality}" — verify the output tone matches.`);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
return numbered.join('\n');
|
|
729
|
+
}
|
|
730
|
+
|
|
684
731
|
function buildDesignContext(manifest: Record<string, any>): PrepareResult['design_context'] {
|
|
685
732
|
const design = manifest.design;
|
|
686
733
|
if (!design) return undefined;
|
|
687
734
|
const complexity = (design.complexity as 'restrained' | 'balanced' | 'elaborate') ?? 'balanced';
|
|
735
|
+
const tier = (design.quality_tier as 'mvp' | 'production' | 'flagship') ?? 'production';
|
|
688
736
|
return {
|
|
689
737
|
...(design.personality ? { personality: design.personality } : {}),
|
|
690
738
|
complexity,
|
|
739
|
+
quality_tier: tier,
|
|
691
740
|
...(design.audience ? { audience: design.audience } : {}),
|
|
692
741
|
complexity_rule: complexityRule(complexity),
|
|
742
|
+
quality_tier_rule: qualityTierRule(tier),
|
|
743
|
+
quality_test: buildQualityTest(complexity, tier, design.personality),
|
|
693
744
|
};
|
|
694
745
|
}
|
|
695
746
|
|
|
@@ -710,10 +761,10 @@ function buildAntiPatterns(
|
|
|
710
761
|
|
|
711
762
|
// Contract-specific must_avoid
|
|
712
763
|
const contract_specific: Record<string, string[]> = {};
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
764
|
+
const contractsDir = resolve(projectDir, manifest.includes?.contracts ?? './contracts/');
|
|
765
|
+
if (existsSync(contractsDir)) {
|
|
766
|
+
for (const file of readdirSync(contractsDir).filter((f) => f.endsWith('.yaml') && !f.startsWith('x_'))) {
|
|
767
|
+
try {
|
|
717
768
|
const content = YAML.parse(readFileSync(join(contractsDir, file), 'utf-8'));
|
|
718
769
|
const contractName = Object.keys(content)[0];
|
|
719
770
|
const mustAvoid: string[] = content[contractName]?.generation?.must_avoid ?? [];
|
|
@@ -721,9 +772,11 @@ function buildAntiPatterns(
|
|
|
721
772
|
const filtered = mustAvoid.filter((item: string) => matchesTargetPlatform(item, target));
|
|
722
773
|
if (filtered.length > 0) contract_specific[contractName] = filtered;
|
|
723
774
|
}
|
|
775
|
+
} catch {
|
|
776
|
+
continue;
|
|
724
777
|
}
|
|
725
778
|
}
|
|
726
|
-
}
|
|
779
|
+
}
|
|
727
780
|
|
|
728
781
|
// Project-specific avoid from design section
|
|
729
782
|
const project_specific: string[] = [];
|
|
@@ -1386,6 +1439,8 @@ function buildUpdatePrepareResult(cwd: string, target: string, includeContents:
|
|
|
1386
1439
|
const sharedLayerConfigs = sharedLayersForTarget(projectDir, target);
|
|
1387
1440
|
const codeRoots = suggestCodeRoots(target, outputDir, projectDir, sharedLayerConfigs);
|
|
1388
1441
|
const manifest = readManifest(projectDir);
|
|
1442
|
+
const antiPatterns = buildAntiPatterns(manifest, projectDir, target);
|
|
1443
|
+
const designContext = buildDesignContext(manifest);
|
|
1389
1444
|
const sharedLayerInfos = buildSharedLayerInfos(projectDir, target, sharedLayerConfigs);
|
|
1390
1445
|
const platformDef = readPlatformDefinition(projectDir, manifest, target);
|
|
1391
1446
|
const platformConfig = buildPlatformConfig(target, platformDef);
|
|
@@ -1429,6 +1484,8 @@ function buildUpdatePrepareResult(cwd: string, target: string, includeContents:
|
|
|
1429
1484
|
items,
|
|
1430
1485
|
...(sharedLayerInfos.length > 0 ? { shared_layers: sharedLayerInfos } : {}),
|
|
1431
1486
|
...(includeContents ? { spec_contents: readAllSpecContents(projectDir) } : {}),
|
|
1487
|
+
...(antiPatterns ? { anti_patterns: antiPatterns } : {}),
|
|
1488
|
+
...(designContext ? { design_context: designContext } : {}),
|
|
1432
1489
|
next_steps: nextSteps,
|
|
1433
1490
|
};
|
|
1434
1491
|
}
|
|
@@ -271,6 +271,12 @@
|
|
|
271
271
|
"description": "How elaborate animations, effects, and visual details should be"
|
|
272
272
|
},
|
|
273
273
|
"audience": { "type": "string", "description": "Who uses this app — informs tone and complexity" },
|
|
274
|
+
"quality_tier": {
|
|
275
|
+
"type": "string",
|
|
276
|
+
"enum": ["mvp", "production", "flagship"],
|
|
277
|
+
"default": "production",
|
|
278
|
+
"description": "Quality bar — mvp: functional-only, production: polished with full tokens, flagship: pixel-perfect with every state and motion pattern"
|
|
279
|
+
},
|
|
274
280
|
"avoid": {
|
|
275
281
|
"type": "array",
|
|
276
282
|
"items": { "type": "string" },
|
|
@@ -293,6 +299,8 @@
|
|
|
293
299
|
"motion": { "type": "array", "items": { "type": "string" } },
|
|
294
300
|
"elevation": { "type": "array", "items": { "type": "string" } },
|
|
295
301
|
"layout": { "type": "array", "items": { "type": "string" } },
|
|
302
|
+
"visual": { "type": "array", "items": { "type": "string" } },
|
|
303
|
+
"interaction": { "type": "array", "items": { "type": "string" } },
|
|
296
304
|
"accessibility": { "type": "array", "items": { "type": "string" } }
|
|
297
305
|
},
|
|
298
306
|
"additionalProperties": false
|
package/schema/validate.ts
CHANGED
|
@@ -399,6 +399,16 @@ function buildAjv(): AjvInstance {
|
|
|
399
399
|
}
|
|
400
400
|
|
|
401
401
|
const BASE = "https://openuispec.rsteam.uz/schema/";
|
|
402
|
+
const TOKEN_FILE_SCHEMAS: Record<string, string> = {
|
|
403
|
+
"color.yaml": "color.schema.json",
|
|
404
|
+
"typography.yaml": "typography.schema.json",
|
|
405
|
+
"spacing.yaml": "spacing.schema.json",
|
|
406
|
+
"elevation.yaml": "elevation.schema.json",
|
|
407
|
+
"motion.yaml": "motion.schema.json",
|
|
408
|
+
"layout.yaml": "layout.schema.json",
|
|
409
|
+
"themes.yaml": "themes.schema.json",
|
|
410
|
+
"icons.yaml": "icons.schema.json",
|
|
411
|
+
};
|
|
402
412
|
|
|
403
413
|
// ── validate one file ────────────────────────────────────────────────
|
|
404
414
|
|
|
@@ -536,20 +546,13 @@ const GROUPS: Record<string, ValidationGroup> = {
|
|
|
536
546
|
run(ajv, projectDir, includes) {
|
|
537
547
|
let errors = 0;
|
|
538
548
|
const tokensDir = resolveInclude(projectDir, includes.tokens);
|
|
539
|
-
const
|
|
540
|
-
"color.yaml": "color.schema.json",
|
|
541
|
-
"typography.yaml": "typography.schema.json",
|
|
542
|
-
"spacing.yaml": "spacing.schema.json",
|
|
543
|
-
"elevation.yaml": "elevation.schema.json",
|
|
544
|
-
"motion.yaml": "motion.schema.json",
|
|
545
|
-
"layout.yaml": "layout.schema.json",
|
|
546
|
-
"themes.yaml": "themes.schema.json",
|
|
547
|
-
"icons.yaml": "icons.schema.json",
|
|
548
|
-
};
|
|
549
|
-
for (const [data, schema] of Object.entries(tokenMap)) {
|
|
549
|
+
for (const [data, schema] of Object.entries(TOKEN_FILE_SCHEMAS)) {
|
|
550
550
|
const filePath = join(tokensDir, data);
|
|
551
551
|
if (existsSync(filePath)) {
|
|
552
552
|
errors += validateFile(ajv, filePath, `${BASE}tokens/${schema}`);
|
|
553
|
+
} else {
|
|
554
|
+
console.log(` FAIL ${data} (required token file is missing)`);
|
|
555
|
+
errors += 1;
|
|
553
556
|
}
|
|
554
557
|
}
|
|
555
558
|
return errors;
|
|
@@ -557,20 +560,16 @@ const GROUPS: Record<string, ValidationGroup> = {
|
|
|
557
560
|
collectJson(ajv, projectDir, includes, groupKey) {
|
|
558
561
|
const errors: JsonError[] = [];
|
|
559
562
|
const tokensDir = resolveInclude(projectDir, includes.tokens);
|
|
560
|
-
const
|
|
561
|
-
"color.yaml": "color.schema.json",
|
|
562
|
-
"typography.yaml": "typography.schema.json",
|
|
563
|
-
"spacing.yaml": "spacing.schema.json",
|
|
564
|
-
"elevation.yaml": "elevation.schema.json",
|
|
565
|
-
"motion.yaml": "motion.schema.json",
|
|
566
|
-
"layout.yaml": "layout.schema.json",
|
|
567
|
-
"themes.yaml": "themes.schema.json",
|
|
568
|
-
"icons.yaml": "icons.schema.json",
|
|
569
|
-
};
|
|
570
|
-
for (const [data, schema] of Object.entries(tokenMap)) {
|
|
563
|
+
for (const [data, schema] of Object.entries(TOKEN_FILE_SCHEMAS)) {
|
|
571
564
|
const filePath = join(tokensDir, data);
|
|
572
565
|
if (existsSync(filePath)) {
|
|
573
566
|
errors.push(...collectValidateFile(ajv, filePath, `${BASE}tokens/${schema}`));
|
|
567
|
+
} else {
|
|
568
|
+
errors.push({
|
|
569
|
+
file: data,
|
|
570
|
+
path: "(root)",
|
|
571
|
+
message: "required token file is missing",
|
|
572
|
+
});
|
|
574
573
|
}
|
|
575
574
|
}
|
|
576
575
|
return { group: groupKey, errors };
|
package/spec/openuispec-v0.2.md
CHANGED
|
@@ -92,19 +92,23 @@ generation:
|
|
|
92
92
|
ios: { language: swift, framework: swiftui }
|
|
93
93
|
android: { language: kotlin, framework: compose }
|
|
94
94
|
web: { language: typescript, framework: react }
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
95
|
+
|
|
96
|
+
generation_guidance: # Section 16.2
|
|
97
|
+
universal_anti_patterns:
|
|
98
|
+
typography:
|
|
99
|
+
- "Do not fall back to Inter, Roboto, Arial, or system defaults"
|
|
100
|
+
color:
|
|
101
|
+
- "Do not use pure black (#000000) or pure white (#FFFFFF)"
|
|
102
|
+
# ... additional domains: spacing, motion, elevation, layout, visual, interaction, accessibility
|
|
103
|
+
audit_threshold: 70
|
|
104
|
+
|
|
105
|
+
design: # Section 16.1
|
|
106
|
+
personality: "Clean, focused..."
|
|
107
|
+
complexity: "balanced" # restrained | balanced | elaborate
|
|
108
|
+
quality_tier: "production" # mvp | production | flagship
|
|
109
|
+
audience: "..."
|
|
110
|
+
avoid:
|
|
111
|
+
- "Do not use decorative gradients"
|
|
108
112
|
```
|
|
109
113
|
|
|
110
114
|
---
|
|
@@ -4098,6 +4102,148 @@ Each `.yaml` file in the components directory defines one component. The file mu
|
|
|
4098
4102
|
|
|
4099
4103
|
---
|
|
4100
4104
|
|
|
4105
|
+
## 16. Design intent and generation guidance
|
|
4106
|
+
|
|
4107
|
+
This section defines how the manifest communicates design intent, anti-patterns, and quality expectations to AI generators.
|
|
4108
|
+
|
|
4109
|
+
### 16.1 Design section
|
|
4110
|
+
|
|
4111
|
+
The `design` section in `openuispec.yaml` captures the project's visual identity and quality bar:
|
|
4112
|
+
|
|
4113
|
+
```yaml
|
|
4114
|
+
design:
|
|
4115
|
+
personality: "Minimal and calm — clarity over decoration"
|
|
4116
|
+
complexity: "restrained" # restrained | balanced | elaborate
|
|
4117
|
+
quality_tier: "production" # mvp | production | flagship
|
|
4118
|
+
audience: "Professionals who prefer low-distraction tools"
|
|
4119
|
+
avoid:
|
|
4120
|
+
- "Do not add decorative illustrations or background patterns"
|
|
4121
|
+
- "[web] Do not use CSS animations for non-interactive purposes"
|
|
4122
|
+
```
|
|
4123
|
+
|
|
4124
|
+
| Field | Type | Default | Description |
|
|
4125
|
+
|-------|------|---------|-------------|
|
|
4126
|
+
| `personality` | string | — | Brief description of the brand's visual personality |
|
|
4127
|
+
| `complexity` | enum | `balanced` | How elaborate animations, effects, and visual details should be |
|
|
4128
|
+
| `quality_tier` | enum | `production` | Quality bar for this project |
|
|
4129
|
+
| `audience` | string | — | Who uses this app — informs tone and complexity |
|
|
4130
|
+
| `avoid` | string[] | — | Project-specific anti-patterns. May use `[web]`/`[ios]`/`[android]` scope tags |
|
|
4131
|
+
|
|
4132
|
+
**Complexity levels:**
|
|
4133
|
+
|
|
4134
|
+
| Level | Meaning |
|
|
4135
|
+
|-------|---------|
|
|
4136
|
+
| `restrained` | Minimal motion (required state transitions only). No decorative shadows. Clean whitespace. No background effects. |
|
|
4137
|
+
| `balanced` | Apply all motion patterns. Use elevation tokens fully. Standard state animations. |
|
|
4138
|
+
| `elaborate` | Rich animations with staggered reveals. Creative elevation. Platform-specific flourishes. |
|
|
4139
|
+
|
|
4140
|
+
**Quality tiers:**
|
|
4141
|
+
|
|
4142
|
+
| Tier | Meaning |
|
|
4143
|
+
|------|---------|
|
|
4144
|
+
| `mvp` | Functional-only. Use semantic tokens but tolerate simple layouts. Skip elevation, motion patterns, and adaptive breakpoints. |
|
|
4145
|
+
| `production` | Production-quality. Apply all tokens, handle accessibility, support adaptive breakpoints. Motion and elevation expected. |
|
|
4146
|
+
| `flagship` | Pixel-perfect. Every token, motion pattern, elevation level, and adaptive breakpoint must be implemented. All contract states required. No shortcuts. |
|
|
4147
|
+
|
|
4148
|
+
### 16.2 Generation guidance
|
|
4149
|
+
|
|
4150
|
+
The `generation_guidance` section in `openuispec.yaml` provides cross-contract anti-patterns and quality thresholds:
|
|
4151
|
+
|
|
4152
|
+
```yaml
|
|
4153
|
+
generation_guidance:
|
|
4154
|
+
universal_anti_patterns:
|
|
4155
|
+
typography:
|
|
4156
|
+
- "Do not fall back to Inter, Roboto, Arial, or system defaults when the spec defines a custom font_family"
|
|
4157
|
+
color:
|
|
4158
|
+
- "Do not use pure black (#000000) or pure white (#FFFFFF)"
|
|
4159
|
+
visual:
|
|
4160
|
+
- "Do not apply gradient text — it fails contrast checks"
|
|
4161
|
+
interaction:
|
|
4162
|
+
- "Do not treat hover and focus as the same state"
|
|
4163
|
+
# ... additional domains
|
|
4164
|
+
audit_threshold: 70
|
|
4165
|
+
```
|
|
4166
|
+
|
|
4167
|
+
**Anti-pattern domains:**
|
|
4168
|
+
|
|
4169
|
+
| Domain | Scope |
|
|
4170
|
+
|--------|-------|
|
|
4171
|
+
| `typography` | Font choices, weights, scale usage |
|
|
4172
|
+
| `color` | Palettes, contrast, pure values |
|
|
4173
|
+
| `spacing` | Scale adherence, alias usage |
|
|
4174
|
+
| `motion` | Easing, duration, reduced-motion |
|
|
4175
|
+
| `elevation` | Shadow usage, depth hierarchy |
|
|
4176
|
+
| `layout` | Size classes, breakpoints, card usage |
|
|
4177
|
+
| `visual` | Decoration, gradients, glassmorphism |
|
|
4178
|
+
| `interaction` | State handling, focus vs hover, gestures |
|
|
4179
|
+
| `accessibility` | Color-only differentiation, focus rings, tab order |
|
|
4180
|
+
|
|
4181
|
+
Anti-patterns are scoped with platform tags (`[web]`, `[ios]`, `[android]`). The `prepare` command filters them to the target platform before delivery to the generator.
|
|
4182
|
+
|
|
4183
|
+
Anti-patterns exist at three levels:
|
|
4184
|
+
1. **Universal** — `generation_guidance.universal_anti_patterns` in the manifest (cross-contract)
|
|
4185
|
+
2. **Contract-specific** — `generation.must_avoid` in each contract file
|
|
4186
|
+
3. **Project-specific** — `design.avoid` in the manifest
|
|
4187
|
+
|
|
4188
|
+
### 16.3 Design quality audit
|
|
4189
|
+
|
|
4190
|
+
The `check --audit` command scores the spec against design quality heuristics.
|
|
4191
|
+
|
|
4192
|
+
**Score formula:** `max(0, 100 - errors × 10 - warnings × 3)`
|
|
4193
|
+
|
|
4194
|
+
**Checks performed:**
|
|
4195
|
+
|
|
4196
|
+
| Domain | Rule | Severity | What it catches |
|
|
4197
|
+
|--------|------|----------|----------------|
|
|
4198
|
+
| tokens | `missing_file` | error | Required token file not found |
|
|
4199
|
+
| typography | `font_diversity` | error | Primary font is an AI default (Inter, Roboto, Arial, Open Sans) |
|
|
4200
|
+
| typography | `scale_usage` | warning | Fewer than 4 type scale levels |
|
|
4201
|
+
| typography | `weight_hierarchy` | warning | Single font weight across the entire type scale |
|
|
4202
|
+
| color | `pure_black` | error | Literal #000000 in token values |
|
|
4203
|
+
| color | `pure_white` | error | Literal #FFFFFF in token values |
|
|
4204
|
+
| color | `semantic_completeness` | warning | Missing success, warning, danger, or info semantic color |
|
|
4205
|
+
| color | `theme_coverage` | warning | Missing light or dark theme |
|
|
4206
|
+
| spacing | `scale_usage` | warning | Fewer than 4 spacing scale values |
|
|
4207
|
+
| spacing | `alias_page_margin` | warning | No page_margin alias defined |
|
|
4208
|
+
| spacing | `alias_card_padding` | warning | No card_padding alias defined |
|
|
4209
|
+
| motion | `duration_variety` | warning | Single duration value for all animations |
|
|
4210
|
+
| motion | `reduced_motion` | error | No reduced_motion policy |
|
|
4211
|
+
| motion | `easing_quality` | warning | Missing enter/exit curves or no cubic-bezier easing |
|
|
4212
|
+
| elevation | `level_count` | warning | Fewer than 2 non-none elevation levels |
|
|
4213
|
+
| elevation | `progression` | warning | Elevation levels not monotonically increasing |
|
|
4214
|
+
| layout | `size_class_coverage` | warning | Fewer than 2 size classes or no compact class |
|
|
4215
|
+
| contracts | `collection_empty_state` | warning | Collection missing empty_state in must_handle |
|
|
4216
|
+
| contracts | `state_coverage` | warning | Contract with empty must_handle |
|
|
4217
|
+
|
|
4218
|
+
The `audit_threshold` in `generation_guidance` sets the project-wide minimum score. The `--min-score` CLI flag overrides it per-run.
|
|
4219
|
+
|
|
4220
|
+
### 16.4 Prepare output
|
|
4221
|
+
|
|
4222
|
+
The `prepare` command includes `design_context` and `anti_patterns` in its output for AI generators:
|
|
4223
|
+
|
|
4224
|
+
```json
|
|
4225
|
+
{
|
|
4226
|
+
"design_context": {
|
|
4227
|
+
"personality": "Minimal and calm...",
|
|
4228
|
+
"complexity": "restrained",
|
|
4229
|
+
"quality_tier": "production",
|
|
4230
|
+
"audience": "...",
|
|
4231
|
+
"complexity_rule": "Minimal motion (required state transitions only)...",
|
|
4232
|
+
"quality_tier_rule": "Production-quality. Apply all tokens...",
|
|
4233
|
+
"quality_test": "After generation, verify the output does NOT exhibit these AI-slop indicators:\n1. Inter/Roboto/Arial as the primary font..."
|
|
4234
|
+
},
|
|
4235
|
+
"anti_patterns": {
|
|
4236
|
+
"universal": { "typography": ["..."], "color": ["..."] },
|
|
4237
|
+
"contract_specific": { "action_trigger": ["..."] },
|
|
4238
|
+
"project_specific": ["..."]
|
|
4239
|
+
}
|
|
4240
|
+
}
|
|
4241
|
+
```
|
|
4242
|
+
|
|
4243
|
+
The `quality_test` field is an auto-generated checklist tuned to the project's complexity and quality tier. AI generators should use it as a post-generation self-review step.
|
|
4244
|
+
|
|
4245
|
+
---
|
|
4246
|
+
|
|
4101
4247
|
## Appendix A: Type reference
|
|
4102
4248
|
|
|
4103
4249
|
| Type | Description | Example |
|