openuispec 0.2.15 → 0.2.17

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.
Files changed (50) hide show
  1. package/README.md +4 -2
  2. package/check/audit.ts +291 -0
  3. package/check/index.ts +19 -3
  4. package/docs/cli.md +29 -3
  5. package/docs/file-formats.md +83 -0
  6. package/docs/implementation-notes.md +8 -0
  7. package/examples/social-app/openuispec/contracts/action_trigger.yaml +8 -0
  8. package/examples/social-app/openuispec/contracts/collection.yaml +8 -0
  9. package/examples/social-app/openuispec/contracts/data_display.yaml +8 -0
  10. package/examples/social-app/openuispec/contracts/feedback.yaml +8 -0
  11. package/examples/social-app/openuispec/contracts/input_field.yaml +8 -0
  12. package/examples/social-app/openuispec/contracts/nav_container.yaml +9 -0
  13. package/examples/social-app/openuispec/contracts/surface.yaml +8 -0
  14. package/examples/social-app/openuispec/openuispec.yaml +40 -0
  15. package/examples/social-app/openuispec/tokens/color.yaml +4 -0
  16. package/examples/social-app/openuispec/tokens/motion.yaml +4 -0
  17. package/examples/social-app/openuispec/tokens/typography.yaml +11 -0
  18. package/examples/taskflow/openuispec/contracts/action_trigger.yaml +9 -1
  19. package/examples/taskflow/openuispec/contracts/collection.yaml +9 -1
  20. package/examples/taskflow/openuispec/contracts/data_display.yaml +9 -1
  21. package/examples/taskflow/openuispec/contracts/feedback.yaml +9 -1
  22. package/examples/taskflow/openuispec/contracts/input_field.yaml +8 -0
  23. package/examples/taskflow/openuispec/contracts/nav_container.yaml +10 -1
  24. package/examples/taskflow/openuispec/contracts/surface.yaml +9 -1
  25. package/examples/taskflow/openuispec/openuispec.yaml +40 -0
  26. package/examples/taskflow/openuispec/tokens/color.yaml +4 -0
  27. package/examples/taskflow/openuispec/tokens/motion.yaml +4 -0
  28. package/examples/taskflow/openuispec/tokens/typography.yaml +11 -0
  29. package/examples/todo-orbit/openuispec/contracts/action_trigger.yaml +7 -0
  30. package/examples/todo-orbit/openuispec/contracts/collection.yaml +7 -0
  31. package/examples/todo-orbit/openuispec/contracts/data_display.yaml +7 -0
  32. package/examples/todo-orbit/openuispec/contracts/feedback.yaml +7 -0
  33. package/examples/todo-orbit/openuispec/contracts/input_field.yaml +7 -0
  34. package/examples/todo-orbit/openuispec/contracts/nav_container.yaml +8 -0
  35. package/examples/todo-orbit/openuispec/contracts/surface.yaml +7 -0
  36. package/examples/todo-orbit/openuispec/openuispec.yaml +40 -0
  37. package/examples/todo-orbit/openuispec/tokens/color.yaml +4 -0
  38. package/examples/todo-orbit/openuispec/tokens/motion.yaml +4 -0
  39. package/examples/todo-orbit/openuispec/tokens/typography.yaml +11 -0
  40. package/mcp-server/index.ts +15 -3
  41. package/package.json +1 -1
  42. package/prepare/index.ts +102 -0
  43. package/schema/component.schema.json +5 -0
  44. package/schema/contract.schema.json +11 -1
  45. package/schema/custom-contract.schema.json +5 -0
  46. package/schema/openuispec.schema.json +47 -0
  47. package/schema/tokens/color.schema.json +5 -0
  48. package/schema/tokens/motion.schema.json +5 -0
  49. package/schema/tokens/typography.schema.json +10 -0
  50. package/schema/validate.ts +21 -22
package/README.md CHANGED
@@ -46,7 +46,7 @@ This scaffolds a spec directory, starter tokens, and **configures the MCP server
46
46
  ## Key concepts
47
47
 
48
48
  - **Tokens** — design values (color, typography, spacing, elevation, motion) with semantic names and constrained ranges
49
- - **Contracts** — 7 reusable UI component families defined by role, props, interaction states, and accessibility
49
+ - **Contracts** — 7 reusable UI component families defined by role, props, interaction states, accessibility, and `must_avoid` anti-patterns for AI generators
50
50
  - **Components** — reusable compositions of contracts with named slots, states, and variants
51
51
  - **Screens** — compositions of contracts and components with data bindings, adaptive layout, and conditional rendering
52
52
  - **Flows** — multi-screen navigation journeys, intent-based and platform-agnostic
@@ -54,6 +54,8 @@ 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
59
 
58
60
  ## The 7 contract families
59
61
 
@@ -75,7 +77,7 @@ OpenUISpec includes an **MCP server** that AI assistants call automatically duri
75
77
  openuispec init → configures MCP for your agent → AI calls tools automatically
76
78
  ```
77
79
 
78
- When you ask your AI to "add a settings page" or "update the home feed," the MCP server provides spec context before generation, feeds authoritative spec contents during generation, validates spec integrity after edits, and returns a spec-derived checklist for the AI to review the generated code against.
80
+ When you ask your AI to "add a settings page" or "update the home feed," the MCP server provides spec context before generation, feeds authoritative spec contents during generation, validates spec integrity after edits, and returns a spec-derived checklist — including `must_avoid` anti-patterns and design quality score — for the AI to review the generated code against.
79
81
 
80
82
  16 tools are available as both MCP tools and CLI commands — see the [full reference](./docs/cli.md).
81
83
 
package/check/audit.ts ADDED
@@ -0,0 +1,291 @@
1
+ /**
2
+ * Design quality audit for OpenUISpec projects.
3
+ *
4
+ * Checks token patterns and contract completeness to produce a numeric quality score.
5
+ * Score formula: max(0, 100 - errors × 10 - warnings × 3)
6
+ *
7
+ * Usage:
8
+ * openuispec check --target web --audit
9
+ * openuispec check --target ios --audit --min-score 70
10
+ * openuispec check --target web --audit --format json
11
+ */
12
+
13
+ import { existsSync, readFileSync } from "node:fs";
14
+ import { join, resolve } from "node:path";
15
+ import YAML from "yaml";
16
+
17
+ export interface AuditFinding {
18
+ domain: string;
19
+ rule: string;
20
+ severity: "error" | "warning";
21
+ message: string;
22
+ }
23
+
24
+ export interface AuditResult {
25
+ score: number;
26
+ errors: number;
27
+ warnings: number;
28
+ findings: AuditFinding[];
29
+ passed: boolean;
30
+ threshold: number;
31
+ }
32
+
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
+ ];
44
+
45
+ function readYaml(path: string): any {
46
+ try {
47
+ return YAML.parse(readFileSync(path, "utf-8"));
48
+ } catch {
49
+ return null;
50
+ }
51
+ }
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
+
81
+ function checkTypography(tokensDir: string, findings: AuditFinding[]): void {
82
+ const doc = readYamlForAudit(join(tokensDir, "typography.yaml"), "typography", findings);
83
+ if (!doc?.typography) return;
84
+
85
+ // Font diversity: primary must NOT be a common AI default
86
+ const primaryFont = doc.typography.font_family?.primary?.value;
87
+ if (typeof primaryFont === "string" && AI_DEFAULT_FONTS.has(primaryFont)) {
88
+ findings.push({
89
+ domain: "typography",
90
+ rule: "font_diversity",
91
+ severity: "error",
92
+ message: `Primary font "${primaryFont}" is an AI-default choice. Use a distinctive brand font.`,
93
+ });
94
+ }
95
+
96
+ // Scale usage: at least 4 distinct scale levels defined
97
+ const scaleKeys = Object.keys(doc.typography.scale ?? {});
98
+ if (scaleKeys.length > 0 && scaleKeys.length < 4) {
99
+ findings.push({
100
+ domain: "typography",
101
+ rule: "scale_usage",
102
+ severity: "warning",
103
+ message: `Only ${scaleKeys.length} type scale level(s) defined. Use ≥4 distinct levels for clear hierarchy.`,
104
+ });
105
+ }
106
+ }
107
+
108
+ function checkColor(tokensDir: string, findings: AuditFinding[]): void {
109
+ const doc = readYamlForAudit(join(tokensDir, "color.yaml"), "color", findings);
110
+ if (!doc?.color) return;
111
+
112
+ // Pure black/white check
113
+ function scanForPure(obj: any, path: string): void {
114
+ if (typeof obj !== "object" || obj === null) return;
115
+ if (typeof obj.reference === "string") {
116
+ const ref = obj.reference.toUpperCase();
117
+ if (ref === "#000000" || ref === "#000") {
118
+ findings.push({
119
+ domain: "color",
120
+ rule: "pure_black",
121
+ severity: "error",
122
+ message: `Token at ${path} uses pure black (#000000). Use a near-black with hue instead.`,
123
+ });
124
+ }
125
+ if (ref === "#FFFFFF" || ref === "#FFF") {
126
+ findings.push({
127
+ domain: "color",
128
+ rule: "pure_white",
129
+ severity: "error",
130
+ message: `Token at ${path} uses pure white (#FFFFFF). Use a slightly tinted white instead.`,
131
+ });
132
+ }
133
+ }
134
+ for (const [key, value] of Object.entries(obj)) {
135
+ if (key !== "reference" && typeof value === "object") {
136
+ scanForPure(value, `${path}.${key}`);
137
+ }
138
+ }
139
+ }
140
+ scanForPure(doc.color, "color");
141
+
142
+ // Theme coverage: check themes.yaml for both light + dark
143
+ {
144
+ const themes = readYamlForAudit(join(tokensDir, "themes.yaml"), "color", findings);
145
+ if (!themes?.themes) return;
146
+ const themeKeys = Object.keys(themes.themes);
147
+ const hasLight = themeKeys.some((k) => k.includes("light"));
148
+ const hasDark = themeKeys.some((k) => k.includes("dark"));
149
+ if (!hasLight || !hasDark) {
150
+ findings.push({
151
+ domain: "color",
152
+ rule: "theme_coverage",
153
+ severity: "warning",
154
+ message: "Both light and dark themes should be defined in tokens/themes.yaml.",
155
+ });
156
+ }
157
+ }
158
+ }
159
+
160
+ function checkSpacing(tokensDir: string, findings: AuditFinding[]): void {
161
+ const doc = readYamlForAudit(join(tokensDir, "spacing.yaml"), "spacing", findings);
162
+ if (!doc?.spacing) return;
163
+
164
+ // Scale usage: at least 4 distinct values
165
+ const scale = doc.spacing.scale ?? {};
166
+ const scaleCount = Object.keys(scale).length;
167
+ if (scaleCount > 0 && scaleCount < 4) {
168
+ findings.push({
169
+ domain: "spacing",
170
+ rule: "scale_usage",
171
+ severity: "warning",
172
+ message: `Only ${scaleCount} spacing scale value(s) defined. Define ≥4 for meaningful spatial rhythm.`,
173
+ });
174
+ }
175
+
176
+ // Alias usage: page_margin and card_padding should exist
177
+ const aliases = doc.spacing.aliases ?? {};
178
+ const aliasKeys = Object.keys(aliases).map((k) => k.toLowerCase());
179
+ if (!aliasKeys.some((k) => k.includes("page_margin") || k.includes("page"))) {
180
+ findings.push({
181
+ domain: "spacing",
182
+ rule: "alias_page_margin",
183
+ severity: "warning",
184
+ message: "No page_margin alias found in spacing tokens. Define it for consistent screen padding.",
185
+ });
186
+ }
187
+ if (!aliasKeys.some((k) => k.includes("card_padding") || k.includes("card"))) {
188
+ findings.push({
189
+ domain: "spacing",
190
+ rule: "alias_card_padding",
191
+ severity: "warning",
192
+ message: "No card_padding alias found in spacing tokens. Define it for consistent card spacing.",
193
+ });
194
+ }
195
+ }
196
+
197
+ function checkMotion(tokensDir: string, findings: AuditFinding[]): void {
198
+ const doc = readYamlForAudit(join(tokensDir, "motion.yaml"), "motion", findings);
199
+ if (!doc?.motion) return;
200
+
201
+ // Duration variety: at least 2 distinct durations
202
+ const durations = doc.motion.duration ?? {};
203
+ const distinctDurations = new Set(Object.values(durations));
204
+ if (Object.keys(durations).length > 0 && distinctDurations.size < 2) {
205
+ findings.push({
206
+ domain: "motion",
207
+ rule: "duration_variety",
208
+ severity: "warning",
209
+ message: "Only 1 distinct duration value found. Use ≥2 distinct durations (e.g. quick + normal).",
210
+ });
211
+ }
212
+
213
+ // Reduced motion: must be defined
214
+ if (!doc.motion.reduced_motion) {
215
+ findings.push({
216
+ domain: "motion",
217
+ rule: "reduced_motion",
218
+ severity: "error",
219
+ message: "motion.reduced_motion is not defined. Must specify policy for prefers-reduced-motion.",
220
+ });
221
+ }
222
+ }
223
+
224
+ function checkContracts(contractsDir: string, findings: AuditFinding[]): void {
225
+ // All collections have empty_state in must_handle or variants
226
+ {
227
+ const doc = readYamlForAudit(join(contractsDir, "collection.yaml"), "contracts", findings);
228
+ const collection = doc ? doc[Object.keys(doc)[0]] : null;
229
+ if (collection) {
230
+ const mustHandle: string[] = collection.generation?.must_handle ?? [];
231
+ const hasEmptyState = mustHandle.some((s: string) => s.toLowerCase().includes("empty"));
232
+ if (!hasEmptyState) {
233
+ findings.push({
234
+ domain: "contracts",
235
+ rule: "collection_empty_state",
236
+ severity: "warning",
237
+ message: "collection contract does not list empty_state handling in generation.must_handle.",
238
+ });
239
+ }
240
+ }
241
+ }
242
+ }
243
+
244
+ export function buildAuditResult(projectDir: string, threshold: number = 0): AuditResult {
245
+ const manifest = readYaml(join(projectDir, "openuispec.yaml"));
246
+ const tokensDir = resolve(projectDir, manifest?.includes?.tokens ?? "./tokens/");
247
+ const contractsDir = resolve(projectDir, manifest?.includes?.contracts ?? "./contracts/");
248
+
249
+ // Use audit_threshold from manifest if no CLI override
250
+ const effectiveThreshold = threshold > 0 ? threshold : (manifest?.generation_guidance?.audit_threshold ?? 0);
251
+
252
+ const findings: AuditFinding[] = [];
253
+ checkRequiredTokenFiles(tokensDir, findings);
254
+ checkTypography(tokensDir, findings);
255
+ checkColor(tokensDir, findings);
256
+ checkSpacing(tokensDir, findings);
257
+ checkMotion(tokensDir, findings);
258
+ checkContracts(contractsDir, findings);
259
+
260
+ const errors = findings.filter((f) => f.severity === "error").length;
261
+ const warnings = findings.filter((f) => f.severity === "warning").length;
262
+ const score = Math.max(0, 100 - errors * 10 - warnings * 3);
263
+
264
+ return {
265
+ score,
266
+ errors,
267
+ warnings,
268
+ findings,
269
+ passed: score >= effectiveThreshold,
270
+ threshold: effectiveThreshold,
271
+ };
272
+ }
273
+
274
+ export function formatAuditResult(result: AuditResult): string {
275
+ const lines: string[] = [
276
+ `Design Quality Score: ${result.score}/100`,
277
+ `Errors: ${result.errors} Warnings: ${result.warnings}`,
278
+ result.threshold > 0 ? `Threshold: ${result.threshold} — ${result.passed ? "PASS" : "FAIL"}` : "",
279
+ "",
280
+ ].filter((l) => l !== "" || lines?.length === 0);
281
+
282
+ if (result.findings.length === 0) {
283
+ lines.push("No issues found.");
284
+ } else {
285
+ for (const f of result.findings) {
286
+ lines.push(`[${f.severity.toUpperCase()}] [${f.domain}] ${f.message}`);
287
+ }
288
+ }
289
+
290
+ return lines.join("\n");
291
+ }
package/check/index.ts CHANGED
@@ -29,6 +29,7 @@ import {
29
29
  type JsonGroupResult,
30
30
  } from "../schema/validate.js";
31
31
  import { collectSemanticLint } from "../schema/semantic-lint.js";
32
+ import { buildAuditResult, formatAuditResult, type AuditResult } from './audit.js';
32
33
 
33
34
  // ── types ─────────────────────────────────────────────────────────────
34
35
 
@@ -54,6 +55,7 @@ export interface CheckResult {
54
55
  validation: CheckValidation;
55
56
  semantic: CheckSemantic;
56
57
  prepare: CheckPrepare;
58
+ audit?: AuditResult;
57
59
  }
58
60
 
59
61
  // ── prepare readiness helpers ─────────────────────────────────────────
@@ -178,7 +180,7 @@ function determinePrepare(
178
180
 
179
181
  // ── core (importable, no process.exit) ───────────────────────────────
180
182
 
181
- export function buildCheckResult(target: string, cwd: string = process.cwd()): CheckResult {
183
+ export function buildCheckResult(target: string, cwd: string = process.cwd(), includeAudit: boolean = false): CheckResult {
182
184
  const projectDir = findProjectDir(cwd);
183
185
  const projectName = readProjectName(projectDir);
184
186
  const includes = readIncludes(projectDir);
@@ -210,7 +212,8 @@ export function buildCheckResult(target: string, cwd: string = process.cwd()): C
210
212
  // 3. Prepare readiness
211
213
  const prepare = determinePrepare(projectDir, projectName, target);
212
214
 
213
- return { target, validation, semantic, prepare };
215
+ const audit = includeAudit ? buildAuditResult(projectDir) : undefined;
216
+ return { target, validation, semantic, prepare, audit };
214
217
  }
215
218
 
216
219
  // ── main ──────────────────────────────────────────────────────────────
@@ -227,12 +230,25 @@ export function runCheck(argv: string[]): void {
227
230
  process.exit(1);
228
231
  }
229
232
 
230
- const result = buildCheckResult(target);
233
+ const includeAudit = argv.includes("--audit");
234
+ const minScoreIdx = argv.indexOf("--min-score");
235
+ const minScore = minScoreIdx !== -1 && argv[minScoreIdx + 1] ? parseInt(argv[minScoreIdx + 1], 10) : 0;
236
+
237
+ const result = buildCheckResult(target, undefined, includeAudit);
231
238
 
232
239
  if (isJson) {
233
240
  console.log(JSON.stringify(result, null, 2));
234
241
  } else {
235
242
  printReport(result);
243
+ if (result.audit) {
244
+ console.log("\nDesign Quality Audit");
245
+ console.log("====================");
246
+ console.log(formatAuditResult(result.audit));
247
+ if (minScore > 0 && result.audit.score < minScore) {
248
+ console.error(`\nFAIL: Score ${result.audit.score} is below --min-score ${minScore}`);
249
+ process.exit(1);
250
+ }
251
+ }
236
252
  }
237
253
 
238
254
  // Exit codes: 0 = clean + ready, 2 = validation errors, 1 = config error
package/docs/cli.md CHANGED
@@ -62,8 +62,8 @@ Or run directly: `openuispec mcp`
62
62
  | Tool | What it does |
63
63
  |------|-------------|
64
64
  | `openuispec_validate` | Validate spec files against JSON Schemas, optionally filtered by group |
65
- | `openuispec_check` | Validate spec files (schema + semantic) and check target generation readiness. `audit=true` returns a spec-derived review checklist |
66
- | `openuispec_prepare` | Returns spec context, platform config, constraints. Optional `include_specs` embeds all spec contents |
65
+ | `openuispec_check` | Validate spec files (schema + semantic) and check target generation readiness. `audit=true` returns a spec-derived review checklist including `must_avoid` anti-patterns and a design quality score |
66
+ | `openuispec_prepare` | Returns spec context, platform config, constraints, `anti_patterns`, and `design_context`. Optional `include_specs` embeds all spec contents |
67
67
  | `openuispec_drift` | Detect drift, or `snapshot=true` to create/update baseline |
68
68
 
69
69
  ### Visual verification
@@ -97,7 +97,9 @@ openuispec configure-target <t> [--defaults] # Configure target stack
97
97
  ```bash
98
98
  openuispec validate [group...] [--json] # Validate spec files against JSON Schemas
99
99
  openuispec validate semantic # Lint cross-references (locale keys, icons, contracts, tokens)
100
- openuispec check --target <t> [--json] # Validate spec files + check target generation readiness
100
+ openuispec check --target <t> [--json] # Validate spec files + check target generation readiness
101
+ openuispec check --target <t> --audit # Also run design quality audit (score + findings)
102
+ openuispec check --target <t> --audit --min-score 70 # Fail if score below threshold
101
103
  ```
102
104
 
103
105
  ### Status & generation workflow
@@ -270,3 +272,27 @@ openuispec drift --snapshot --target ios
270
272
  - `prepare` runs in `bootstrap` mode for first-time generation and `update` mode after a snapshot exists
271
273
  - `drift --snapshot` is bookkeeping — it does not prove code matches the spec, and requires the output directory to exist. Only run it after reviewing the generated output.
272
274
  - Run `openuispec status` between targets to see what still needs updating
275
+
276
+ ## Design Quality Audit
277
+
278
+ `openuispec check --audit` scores the spec against design quality heuristics and returns findings grouped by domain.
279
+
280
+ ```bash
281
+ openuispec check --target web --audit
282
+ openuispec check --target ios --audit --min-score 70 # exit 1 if score < 70
283
+ openuispec check --target web --audit --json # machine-readable output
284
+ ```
285
+
286
+ **Score formula:** `max(0, 100 - errors × 10 - warnings × 3)`
287
+
288
+ **Checks performed:**
289
+
290
+ | Domain | What's checked |
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 |
294
+ | 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` |
297
+
298
+ The `audit_threshold` in `generation_guidance` sets the project-wide minimum score. `--min-score` overrides it per-run.
@@ -168,3 +168,86 @@ Mock files are not validated by `openuispec validate` and are excluded from drif
168
168
  | 12. Custom contract extensions | `x_` prefixed domain-specific contracts |
169
169
  | 13. Form validation | Validation rules, field dependencies, cross-field checks |
170
170
  | 14. Development workflow | Dual-workflow model, drift detection, spec as sync layer |
171
+
172
+ ## Manifest: `design` and `generation_guidance`
173
+
174
+ Two new top-level sections in `openuispec.yaml` shape how AI generators approach design quality.
175
+
176
+ ### `design` — Project brand intent
177
+
178
+ ```yaml
179
+ design:
180
+ personality: "Clean, focused, productivity-first — no decorative flourishes"
181
+ complexity: "balanced" # restrained | balanced | elaborate
182
+ audience: "Individual contributors and small teams"
183
+ avoid:
184
+ - "Do not use color gradients — flat, token-driven color only"
185
+ - "[web] Do not use CSS animations for non-interactive decorative purposes"
186
+ ```
187
+
188
+ `complexity` controls how generators apply motion, elevation, and decorative detail:
189
+ - `restrained` — required state transitions only, no decorative shadows, clean whitespace
190
+ - `balanced` — all motion patterns, full elevation token usage, standard state animations
191
+ - `elaborate` — rich animations with staggered reveals, creative elevation, platform flourishes
192
+
193
+ `avoid` items may use `[web]`/`[ios]`/`[android]` scope tags — same convention as `extra_rules`.
194
+
195
+ ### `generation_guidance` — Universal anti-patterns
196
+
197
+ ```yaml
198
+ generation_guidance:
199
+ universal_anti_patterns:
200
+ typography:
201
+ - "Do not fall back to Inter, Roboto, Arial when the spec defines a custom font_family"
202
+ color:
203
+ - "Do not use pure black (#000000) or pure white (#FFFFFF) — resolve through the token layer"
204
+ spacing:
205
+ - "Do not ignore the page_margin and card_padding aliases"
206
+ motion:
207
+ - "Do not ignore reduced_motion — remove animations entirely when the user prefers it"
208
+ elevation:
209
+ - "Do not add shadows to elements that don't specify an elevation token"
210
+ layout:
211
+ - "Do not use pixel breakpoints — reference size classes by name"
212
+ accessibility:
213
+ - "Do not use color as the only differentiator between states"
214
+ audit_threshold: 70 # Minimum score for openuispec check --audit
215
+ ```
216
+
217
+ `universal_anti_patterns` appear in the `openuispec prepare` output under `anti_patterns.universal`, filtered by target platform tag. `audit_threshold` is the default minimum score for `openuispec check --audit`.
218
+
219
+ ## Contract and component: `must_avoid`
220
+
221
+ The `generation` block in contracts, custom contracts, and components now supports `must_avoid` alongside `must_handle`, `should_handle`, and `may_handle`:
222
+
223
+ ```yaml
224
+ action_trigger:
225
+ generation:
226
+ must_avoid:
227
+ - "Do not apply gradient backgrounds to buttons — use flat token-defined colors"
228
+ - "Do not use bounce or elastic easing on press feedback"
229
+ - "[ios] Do not add drop shadows to every button variant"
230
+ ```
231
+
232
+ Items may use `[web]`/`[ios]`/`[android]` scope tags. Untagged items apply to all platforms. `must_avoid` appears in the `openuispec prepare` output under `anti_patterns.contract_specific`, filtered by target.
233
+
234
+ ## Token: `generation_notes`
235
+
236
+ Token entries in `typography`, `color`, and `motion` files support sparse `generation_notes` for AI generators:
237
+
238
+ ```yaml
239
+ typography:
240
+ font_family:
241
+ primary:
242
+ value: "DM Sans"
243
+ generation_notes:
244
+ - "Never fall back to Inter or Roboto — choose a geometric sans with similar x-height"
245
+ - "[web] Use font-display: swap to prevent FOUT"
246
+ - "[ios] Register the font in Info.plist before first render"
247
+ scale:
248
+ body:
249
+ generation_notes:
250
+ - "This is the most-used text style — ensure line_height is comfortable (1.5 default)"
251
+ ```
252
+
253
+ `generation_notes` are sparse — only add them for tokens where AI generators consistently make wrong choices.
@@ -161,3 +161,11 @@
161
161
  9. `openuispec drift --snapshot --target <target>`
162
162
  - `drift --snapshot` should continue to be described as bookkeeping/baselining, not as proof that the target implementation matches the spec.
163
163
  - If the target output directory does not exist yet, the CLI should direct the user to run code generation first instead of implying that snapshot can initialize an empty target.
164
+
165
+ ## Design quality audit
166
+
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
+
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.
170
+
171
+ `generation.extra_rules` in the manifest is included in prepare output and filtered by platform tag.
@@ -3,6 +3,14 @@
3
3
  # Rounded Cap: primary diagonal rounds TR+BL, alternate rounds TL+BR
4
4
 
5
5
  action_trigger:
6
+ generation:
7
+ must_avoid:
8
+ - "Do not apply gradient backgrounds to buttons — use flat token-defined colors"
9
+ - "Do not make primary and secondary variants visually identical — they must be immediately distinguishable by background, border, or text color"
10
+ - "Do not use bounce or elastic easing on press feedback — use the motion.easing tokens"
11
+ - "Do not add drop shadows to every button variant — only elevated contexts use elevation tokens"
12
+ - "Do not use pure black (#000) or pure white (#fff) — always resolve through color.text and color.surface tokens"
13
+ - "Do not set identical min_height across all size variants — sm, md, lg must feel proportionally different"
6
14
  tokens:
7
15
  shape: "rounded_cap_primary"
8
16
  shape_note: "TR+BL pill-rounded (~40-60% of element height), TL+BR sharp (2-4px)"
@@ -2,6 +2,14 @@
2
2
  # Base definition: spec Section 4.7
3
3
 
4
4
  collection:
5
+ generation:
6
+ must_avoid:
7
+ - "Do not omit empty states — every collection must render the empty_state component when data is empty"
8
+ - "Do not use infinite scroll without a visible loading indicator at the bottom"
9
+ - "Do not render loading skeletons that mismatch the actual item layout — skeleton shapes must reflect the item_contract variant"
10
+ - "Do not use identical item spacing for list and grid variants — grid uses gap, list uses separator"
11
+ - "Do not render table headers with the same visual weight as table rows — headers use header_background and header_weight tokens"
12
+ - "[web] Do not skip keyboard navigation support — lists need ArrowUp/ArrowDown, grids need 2D arrow navigation"
5
13
  variants:
6
14
  list:
7
15
  semantic: "Vertical list — items use card shape from data_display"
@@ -3,6 +3,14 @@
3
3
  # Rounded Cap: cards use 3px 20px 3px 20px (primary diagonal, scaled for card size)
4
4
 
5
5
  data_display:
6
+ generation:
7
+ must_avoid:
8
+ - "Do not nest cards inside cards — use flat hierarchy with spacing and subtle background shifts"
9
+ - "Do not give every card variant the same border-radius and shadow — vary by emphasis level"
10
+ - "Do not use gray text on colored backgrounds — use a darker shade of the background color or the on_color token"
11
+ - "Do not make all stat cards identical size with centered text — vary layout to match the data shape"
12
+ - "Do not use the same skeleton shape for all variants — skeleton must match the specific variant layout (card vs compact vs hero)"
13
+ - "Do not omit the highlighted state visual differentiation — unread/new items must be visually distinct from default items"
6
14
  variants:
7
15
  card:
8
16
  semantic: "Content card — rounded cap primary diagonal, subtle border, warm cream background"
@@ -3,6 +3,14 @@
3
3
  # Rounded Cap: toasts and dialogs use primary diagonal
4
4
 
5
5
  feedback:
6
+ generation:
7
+ must_avoid:
8
+ - "Do not use the same visual treatment for all severity levels — info, success, warning, and error must be immediately distinguishable by color, icon, and border"
9
+ - "Do not stack multiple toasts without spacing or queue management — overlapping toasts are a usability failure"
10
+ - "Do not use alert dialogs for non-blocking informational feedback — use toasts or banners per the variant definition"
11
+ - "Do not skip enter/exit animations — feedback appearing or disappearing instantly feels broken"
12
+ - "Do not use the same position for toasts and banners — toasts float, banners are inline"
13
+ - "Do not auto-dismiss error-severity feedback with a short timer — errors need manual dismissal or a longer duration"
6
14
  variants:
7
15
  toast:
8
16
  semantic: "Toast notification — rounded cap primary diagonal, auto-dismiss"
@@ -3,6 +3,14 @@
3
3
  # Rounded Cap: inputs match button diagonal (primary: 2px 24px 2px 24px)
4
4
 
5
5
  input_field:
6
+ generation:
7
+ must_avoid:
8
+ - "Do not use placeholder text as the only label — always render a persistent visible label"
9
+ - "Do not use a single generic gray border for all states — focused, error, and disabled must each have distinct visual treatment"
10
+ - "Do not hardcode red for error states — use color.semantic.danger which may differ from pure red per the token definition"
11
+ - "Do not skip the label animation between resting and active positions — this is a must_handle requirement"
12
+ - "Do not use the same visual weight for required and optional fields — required fields need a visible indicator"
13
+ - "Do not render select fields identically across platforms — respect the render_hint and platform_mapping"
6
14
  tokens:
7
15
  shape: "2px 24px 2px 24px"
8
16
  background: "color.surface.primary"
@@ -2,6 +2,15 @@
2
2
  # Base definition: spec Section 4.4
3
3
 
4
4
  nav_container:
5
+ generation:
6
+ must_avoid:
7
+ - "Do not use the same icon treatment for active and inactive items — differentiate with fill/outline variants, color, or both"
8
+ - "Do not center-align sidebar items — use leading alignment with consistent padding per the item_padding_h token"
9
+ - "Do not ignore the collapsed state for sidebar and rail variants — collapsed mode must show only icons with proper spacing"
10
+ - "Do not use a generic hamburger menu icon when the spec defines a specific drawer trigger"
11
+ - "Do not render tab_bar and sidebar with the same visual weight — tab_bar is compact and subordinate, sidebar is prominent"
12
+ - "[ios] Do not forget safe area insets on tab_bar — iOS bottom inset must be respected"
13
+ - "[android] Do not ignore gesture navigation bar insets on tab_bar and bottom navigation"
5
14
  variants:
6
15
  tab_bar:
7
16
  semantic: "Bottom tab bar — standard platform tab bar with badge support"
@@ -3,6 +3,14 @@
3
3
  # Rounded Cap: surfaces use 3px 24px 3px 24px (primary diagonal, largest scale)
4
4
 
5
5
  surface:
6
+ generation:
7
+ must_avoid:
8
+ - "Do not use the same overlay opacity for modal, sheet, and popover — each has a distinct tokens definition"
9
+ - "Do not omit the drag indicator on sheet variants — it signals that the surface is interactive and dismissible"
10
+ - "Do not use glassmorphism or frosted-glass effects unless the project's design personality explicitly calls for it"
11
+ - "Do not skip focus trapping on modal and sheet variants — keyboard users must not be able to tab behind the surface"
12
+ - "Do not render popover without an arrow/caret pointing to its trigger element"
13
+ - "Do not present fullscreen surfaces without a clear close affordance"
6
14
  tokens:
7
15
  shape: "3px 24px 3px 24px"
8
16
  background: "color.surface.secondary"