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 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 in the manifest captures brand personality, complexity level, and audience — generators match visual elaborateness accordingly
58
- - **Anti-patterns** — `must_avoid` in contracts and `generation_guidance.universal_anti_patterns` in the manifest steer AI away from generic, statistically common design mistakes
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 (15 sections) |
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 = readYaml(join(tokensDir, "typography.yaml"));
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 = readYaml(join(tokensDir, "color.yaml"));
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 = readYaml(join(tokensDir, "themes.yaml"));
107
- const themeKeys = Object.keys(themes?.themes ?? {});
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 = readYaml(join(tokensDir, "spacing.yaml"));
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 = readYaml(join(tokensDir, "motion.yaml"));
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 = readYaml(join(contractsDir, "collection.yaml"));
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
- | Typography | Primary font not an AI default (Inter/Roboto/Arial/Open Sans) · ≥4 scale levels defined |
293
- | Color | No pure #000000/#FFFFFF · Both light + dark themes present |
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 defined |
296
- | Contracts | `collection` has `empty_state` in `must_handle` |
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openuispec",
3
- "version": "0.2.16",
3
+ "version": "0.2.18",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "A semantic UI specification format for AI-native, platform-native app development",
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
- try {
714
- const contractsDir = resolve(projectDir, manifest.includes?.contracts ?? './contracts/');
715
- if (existsSync(contractsDir)) {
716
- for (const file of readdirSync(contractsDir).filter((f) => f.endsWith('.yaml') && !f.startsWith('x_'))) {
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
- } catch { /* skip on error */ }
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
@@ -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 tokenMap: Record<string, string> = {
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 tokenMap: Record<string, string> = {
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 };
@@ -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
- # shared: # optional: cross-platform shared code layers
96
- # mobile_common:
97
- # platforms: [ios, android]
98
- # language: kotlin
99
- # root: "../shared"
100
- # scope: "Business logic, data models, repositories, view models. No UI."
101
- # paths:
102
- # domain: "commonMain/domain/"
103
- # structure: # optional: per-target directory structure (overrides heuristics)
104
- # ios:
105
- # root: "../shared"
106
- # scope: "Pure SwiftUI views and navigation."
107
- # paths: { ui: "iosApp/ui/" }
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 |