openuispec 0.2.14 → 0.2.16
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 -2
- package/check/audit.ts +251 -0
- package/check/index.ts +19 -3
- package/cli/init.ts +1 -0
- package/docs/cli.md +82 -3
- package/docs/file-formats.md +83 -0
- package/docs/implementation-notes.md +8 -0
- package/examples/social-app/openuispec/contracts/action_trigger.yaml +8 -0
- package/examples/social-app/openuispec/contracts/collection.yaml +8 -0
- package/examples/social-app/openuispec/contracts/data_display.yaml +8 -0
- package/examples/social-app/openuispec/contracts/feedback.yaml +8 -0
- package/examples/social-app/openuispec/contracts/input_field.yaml +8 -0
- package/examples/social-app/openuispec/contracts/nav_container.yaml +9 -0
- package/examples/social-app/openuispec/contracts/surface.yaml +8 -0
- package/examples/social-app/openuispec/openuispec.yaml +40 -0
- package/examples/social-app/openuispec/tokens/color.yaml +4 -0
- package/examples/social-app/openuispec/tokens/motion.yaml +4 -0
- package/examples/social-app/openuispec/tokens/typography.yaml +11 -0
- package/examples/taskflow/openuispec/contracts/action_trigger.yaml +9 -1
- package/examples/taskflow/openuispec/contracts/collection.yaml +9 -1
- package/examples/taskflow/openuispec/contracts/data_display.yaml +9 -1
- package/examples/taskflow/openuispec/contracts/feedback.yaml +9 -1
- package/examples/taskflow/openuispec/contracts/input_field.yaml +8 -0
- package/examples/taskflow/openuispec/contracts/nav_container.yaml +10 -1
- package/examples/taskflow/openuispec/contracts/surface.yaml +9 -1
- package/examples/taskflow/openuispec/openuispec.yaml +40 -0
- package/examples/taskflow/openuispec/tokens/color.yaml +4 -0
- package/examples/taskflow/openuispec/tokens/motion.yaml +4 -0
- package/examples/taskflow/openuispec/tokens/typography.yaml +11 -0
- package/examples/todo-orbit/openuispec/contracts/action_trigger.yaml +7 -0
- package/examples/todo-orbit/openuispec/contracts/collection.yaml +7 -0
- package/examples/todo-orbit/openuispec/contracts/data_display.yaml +7 -0
- package/examples/todo-orbit/openuispec/contracts/feedback.yaml +7 -0
- package/examples/todo-orbit/openuispec/contracts/input_field.yaml +7 -0
- package/examples/todo-orbit/openuispec/contracts/nav_container.yaml +8 -0
- package/examples/todo-orbit/openuispec/contracts/surface.yaml +7 -0
- package/examples/todo-orbit/openuispec/openuispec.yaml +40 -0
- package/examples/todo-orbit/openuispec/tokens/color.yaml +4 -0
- package/examples/todo-orbit/openuispec/tokens/motion.yaml +4 -0
- package/examples/todo-orbit/openuispec/tokens/typography.yaml +11 -0
- package/mcp-server/index.ts +22 -6
- package/mcp-server/screenshot-shared.ts +3 -4
- package/mcp-server/screenshot.ts +285 -70
- package/package.json +1 -1
- package/prepare/index.ts +96 -0
- package/schema/component.schema.json +5 -0
- package/schema/contract.schema.json +11 -1
- package/schema/custom-contract.schema.json +5 -0
- package/schema/openuispec.schema.json +47 -0
- package/schema/semantic-lint.ts +5 -3
- package/schema/tokens/color.schema.json +5 -0
- package/schema/tokens/motion.schema.json +5 -0
- package/schema/tokens/typography.schema.json +10 -0
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
|
|
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,251 @@
|
|
|
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 { 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
|
+
|
|
35
|
+
function readYaml(path: string): any {
|
|
36
|
+
try {
|
|
37
|
+
return YAML.parse(readFileSync(path, "utf-8"));
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function checkTypography(tokensDir: string, findings: AuditFinding[]): void {
|
|
44
|
+
const doc = readYaml(join(tokensDir, "typography.yaml"));
|
|
45
|
+
if (!doc?.typography) return;
|
|
46
|
+
|
|
47
|
+
// Font diversity: primary must NOT be a common AI default
|
|
48
|
+
const primaryFont = doc.typography.font_family?.primary?.value;
|
|
49
|
+
if (typeof primaryFont === "string" && AI_DEFAULT_FONTS.has(primaryFont)) {
|
|
50
|
+
findings.push({
|
|
51
|
+
domain: "typography",
|
|
52
|
+
rule: "font_diversity",
|
|
53
|
+
severity: "error",
|
|
54
|
+
message: `Primary font "${primaryFont}" is an AI-default choice. Use a distinctive brand font.`,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Scale usage: at least 4 distinct scale levels defined
|
|
59
|
+
const scaleKeys = Object.keys(doc.typography.scale ?? {});
|
|
60
|
+
if (scaleKeys.length > 0 && scaleKeys.length < 4) {
|
|
61
|
+
findings.push({
|
|
62
|
+
domain: "typography",
|
|
63
|
+
rule: "scale_usage",
|
|
64
|
+
severity: "warning",
|
|
65
|
+
message: `Only ${scaleKeys.length} type scale level(s) defined. Use ≥4 distinct levels for clear hierarchy.`,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function checkColor(tokensDir: string, findings: AuditFinding[]): void {
|
|
71
|
+
const doc = readYaml(join(tokensDir, "color.yaml"));
|
|
72
|
+
if (!doc?.color) return;
|
|
73
|
+
|
|
74
|
+
// Pure black/white check
|
|
75
|
+
function scanForPure(obj: any, path: string): void {
|
|
76
|
+
if (typeof obj !== "object" || obj === null) return;
|
|
77
|
+
if (typeof obj.reference === "string") {
|
|
78
|
+
const ref = obj.reference.toUpperCase();
|
|
79
|
+
if (ref === "#000000" || ref === "#000") {
|
|
80
|
+
findings.push({
|
|
81
|
+
domain: "color",
|
|
82
|
+
rule: "pure_black",
|
|
83
|
+
severity: "error",
|
|
84
|
+
message: `Token at ${path} uses pure black (#000000). Use a near-black with hue instead.`,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
if (ref === "#FFFFFF" || ref === "#FFF") {
|
|
88
|
+
findings.push({
|
|
89
|
+
domain: "color",
|
|
90
|
+
rule: "pure_white",
|
|
91
|
+
severity: "error",
|
|
92
|
+
message: `Token at ${path} uses pure white (#FFFFFF). Use a slightly tinted white instead.`,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
97
|
+
if (key !== "reference" && typeof value === "object") {
|
|
98
|
+
scanForPure(value, `${path}.${key}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
scanForPure(doc.color, "color");
|
|
103
|
+
|
|
104
|
+
// Theme coverage: check themes.yaml for both light + dark
|
|
105
|
+
{
|
|
106
|
+
const themes = readYaml(join(tokensDir, "themes.yaml"));
|
|
107
|
+
const themeKeys = Object.keys(themes?.themes ?? {});
|
|
108
|
+
const hasLight = themeKeys.some((k) => k.includes("light"));
|
|
109
|
+
const hasDark = themeKeys.some((k) => k.includes("dark"));
|
|
110
|
+
if (!hasLight || !hasDark) {
|
|
111
|
+
findings.push({
|
|
112
|
+
domain: "color",
|
|
113
|
+
rule: "theme_coverage",
|
|
114
|
+
severity: "warning",
|
|
115
|
+
message: "Both light and dark themes should be defined in tokens/themes.yaml.",
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function checkSpacing(tokensDir: string, findings: AuditFinding[]): void {
|
|
122
|
+
const doc = readYaml(join(tokensDir, "spacing.yaml"));
|
|
123
|
+
if (!doc?.spacing) return;
|
|
124
|
+
|
|
125
|
+
// Scale usage: at least 4 distinct values
|
|
126
|
+
const scale = doc.spacing.scale ?? {};
|
|
127
|
+
const scaleCount = Object.keys(scale).length;
|
|
128
|
+
if (scaleCount > 0 && scaleCount < 4) {
|
|
129
|
+
findings.push({
|
|
130
|
+
domain: "spacing",
|
|
131
|
+
rule: "scale_usage",
|
|
132
|
+
severity: "warning",
|
|
133
|
+
message: `Only ${scaleCount} spacing scale value(s) defined. Define ≥4 for meaningful spatial rhythm.`,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Alias usage: page_margin and card_padding should exist
|
|
138
|
+
const aliases = doc.spacing.aliases ?? {};
|
|
139
|
+
const aliasKeys = Object.keys(aliases).map((k) => k.toLowerCase());
|
|
140
|
+
if (!aliasKeys.some((k) => k.includes("page_margin") || k.includes("page"))) {
|
|
141
|
+
findings.push({
|
|
142
|
+
domain: "spacing",
|
|
143
|
+
rule: "alias_page_margin",
|
|
144
|
+
severity: "warning",
|
|
145
|
+
message: "No page_margin alias found in spacing tokens. Define it for consistent screen padding.",
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
if (!aliasKeys.some((k) => k.includes("card_padding") || k.includes("card"))) {
|
|
149
|
+
findings.push({
|
|
150
|
+
domain: "spacing",
|
|
151
|
+
rule: "alias_card_padding",
|
|
152
|
+
severity: "warning",
|
|
153
|
+
message: "No card_padding alias found in spacing tokens. Define it for consistent card spacing.",
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function checkMotion(tokensDir: string, findings: AuditFinding[]): void {
|
|
159
|
+
const doc = readYaml(join(tokensDir, "motion.yaml"));
|
|
160
|
+
if (!doc?.motion) return;
|
|
161
|
+
|
|
162
|
+
// Duration variety: at least 2 distinct durations
|
|
163
|
+
const durations = doc.motion.duration ?? {};
|
|
164
|
+
const distinctDurations = new Set(Object.values(durations));
|
|
165
|
+
if (Object.keys(durations).length > 0 && distinctDurations.size < 2) {
|
|
166
|
+
findings.push({
|
|
167
|
+
domain: "motion",
|
|
168
|
+
rule: "duration_variety",
|
|
169
|
+
severity: "warning",
|
|
170
|
+
message: "Only 1 distinct duration value found. Use ≥2 distinct durations (e.g. quick + normal).",
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Reduced motion: must be defined
|
|
175
|
+
if (!doc.motion.reduced_motion) {
|
|
176
|
+
findings.push({
|
|
177
|
+
domain: "motion",
|
|
178
|
+
rule: "reduced_motion",
|
|
179
|
+
severity: "error",
|
|
180
|
+
message: "motion.reduced_motion is not defined. Must specify policy for prefers-reduced-motion.",
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function checkContracts(contractsDir: string, findings: AuditFinding[]): void {
|
|
186
|
+
// All collections have empty_state in must_handle or variants
|
|
187
|
+
{
|
|
188
|
+
const doc = readYaml(join(contractsDir, "collection.yaml"));
|
|
189
|
+
const collection = doc ? doc[Object.keys(doc)[0]] : null;
|
|
190
|
+
if (collection) {
|
|
191
|
+
const mustHandle: string[] = collection.generation?.must_handle ?? [];
|
|
192
|
+
const hasEmptyState = mustHandle.some((s: string) => s.toLowerCase().includes("empty"));
|
|
193
|
+
if (!hasEmptyState) {
|
|
194
|
+
findings.push({
|
|
195
|
+
domain: "contracts",
|
|
196
|
+
rule: "collection_empty_state",
|
|
197
|
+
severity: "warning",
|
|
198
|
+
message: "collection contract does not list empty_state handling in generation.must_handle.",
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function buildAuditResult(projectDir: string, threshold: number = 0): AuditResult {
|
|
206
|
+
const manifest = readYaml(join(projectDir, "openuispec.yaml"));
|
|
207
|
+
const tokensDir = resolve(projectDir, manifest?.includes?.tokens ?? "./tokens/");
|
|
208
|
+
const contractsDir = resolve(projectDir, manifest?.includes?.contracts ?? "./contracts/");
|
|
209
|
+
|
|
210
|
+
// Use audit_threshold from manifest if no CLI override
|
|
211
|
+
const effectiveThreshold = threshold > 0 ? threshold : (manifest?.generation_guidance?.audit_threshold ?? 0);
|
|
212
|
+
|
|
213
|
+
const findings: AuditFinding[] = [];
|
|
214
|
+
checkTypography(tokensDir, findings);
|
|
215
|
+
checkColor(tokensDir, findings);
|
|
216
|
+
checkSpacing(tokensDir, findings);
|
|
217
|
+
checkMotion(tokensDir, findings);
|
|
218
|
+
checkContracts(contractsDir, findings);
|
|
219
|
+
|
|
220
|
+
const errors = findings.filter((f) => f.severity === "error").length;
|
|
221
|
+
const warnings = findings.filter((f) => f.severity === "warning").length;
|
|
222
|
+
const score = Math.max(0, 100 - errors * 10 - warnings * 3);
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
score,
|
|
226
|
+
errors,
|
|
227
|
+
warnings,
|
|
228
|
+
findings,
|
|
229
|
+
passed: score >= effectiveThreshold,
|
|
230
|
+
threshold: effectiveThreshold,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function formatAuditResult(result: AuditResult): string {
|
|
235
|
+
const lines: string[] = [
|
|
236
|
+
`Design Quality Score: ${result.score}/100`,
|
|
237
|
+
`Errors: ${result.errors} Warnings: ${result.warnings}`,
|
|
238
|
+
result.threshold > 0 ? `Threshold: ${result.threshold} — ${result.passed ? "PASS" : "FAIL"}` : "",
|
|
239
|
+
"",
|
|
240
|
+
].filter((l) => l !== "" || lines?.length === 0);
|
|
241
|
+
|
|
242
|
+
if (result.findings.length === 0) {
|
|
243
|
+
lines.push("No issues found.");
|
|
244
|
+
} else {
|
|
245
|
+
for (const f of result.findings) {
|
|
246
|
+
lines.push(`[${f.severity.toUpperCase()}] [${f.domain}] ${f.message}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return lines.join("\n");
|
|
251
|
+
}
|
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
|
-
|
|
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
|
|
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/cli/init.ts
CHANGED
|
@@ -471,6 +471,7 @@ If MCP tools are not available, use these CLI commands with \`--json\` flag:
|
|
|
471
471
|
|
|
472
472
|
**Visual verification:**
|
|
473
473
|
- \`openuispec screenshot --route /path\` — screenshot the web app
|
|
474
|
+
- \`openuispec screenshot --route /path --init-script "..."\` — inject auth/role before rendering (web only; app must implement \`__ous_init\` bootstrapper)
|
|
474
475
|
- \`openuispec screenshot-android [--project-dir path]\` — screenshot Android app
|
|
475
476
|
- \`openuispec screenshot-ios [--project-dir path]\` — screenshot iOS app
|
|
476
477
|
|
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
|
|
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]
|
|
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
|
|
@@ -175,6 +177,59 @@ Each capture supports:
|
|
|
175
177
|
- `wait_for`: per-capture wait time in ms
|
|
176
178
|
- `selector`: CSS selector to screenshot a specific element (web only)
|
|
177
179
|
- `full_page`: capture full scrollable page (web only)
|
|
180
|
+
- `init_script`: JavaScript to execute before the page renders (web only — see below)
|
|
181
|
+
|
|
182
|
+
### `init_script` — app-level initialization
|
|
183
|
+
|
|
184
|
+
`init_script` lets you inject auth, switch roles, or set up session state before a screenshot is taken — without Puppeteer executing JS directly. The tool base64-encodes the script and appends it as a `?__ous_init=<encoded>` query param. The generated app's bootstrapper reads and runs it before rendering.
|
|
185
|
+
|
|
186
|
+
**Why app-level instead of `evaluateOnNewDocument`:** the app can `await` login APIs, set framework state, or call any async init — Puppeteer's `evaluateOnNewDocument` is sync-only and has no access to app internals.
|
|
187
|
+
|
|
188
|
+
**Single capture (MCP):**
|
|
189
|
+
|
|
190
|
+
```json
|
|
191
|
+
{
|
|
192
|
+
"route": "/dashboard",
|
|
193
|
+
"init_script": "window.__auth = { token: 'test-token', role: 'admin' };"
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
**Batch capture (MCP):**
|
|
198
|
+
|
|
199
|
+
```json
|
|
200
|
+
{
|
|
201
|
+
"output_dir": "screenshots",
|
|
202
|
+
"init_script": "window.__auth = { token: 'test-token', role: 'viewer' };",
|
|
203
|
+
"captures": [
|
|
204
|
+
{ "screen": "dashboard", "route": "/dashboard" },
|
|
205
|
+
{ "screen": "admin_panel", "route": "/admin",
|
|
206
|
+
"init_script": "window.__auth = { token: 'test-token', role: 'admin' };" }
|
|
207
|
+
]
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Per-capture `init_script` overrides the shared one. If neither is set, no param is appended and the app renders normally.
|
|
212
|
+
|
|
213
|
+
**Bootstrapper contract** — the generated app must include a bootstrapper that:
|
|
214
|
+
|
|
215
|
+
1. Checks for `__ous_init` in the URL query string on load
|
|
216
|
+
2. Base64-decodes it (`atob`) and `eval`s it (or parses it as structured data)
|
|
217
|
+
3. Runs **before** rendering authenticated content (can be async — app awaits it)
|
|
218
|
+
4. Strips the param from URL/history after processing (`history.replaceState`)
|
|
219
|
+
|
|
220
|
+
Example bootstrapper (framework-agnostic):
|
|
221
|
+
|
|
222
|
+
```js
|
|
223
|
+
const param = new URLSearchParams(location.search).get('__ous_init');
|
|
224
|
+
if (param) {
|
|
225
|
+
try { eval(atob(param)); } catch (e) { console.warn('[ous] init_script error', e); }
|
|
226
|
+
const url = new URL(location.href);
|
|
227
|
+
url.searchParams.delete('__ous_init');
|
|
228
|
+
history.replaceState(null, '', url.toString());
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
This is a **contract between the tool and generated code** — the tool appends the param; the app consumes it.
|
|
178
233
|
|
|
179
234
|
### Preview (experimental)
|
|
180
235
|
|
|
@@ -217,3 +272,27 @@ openuispec drift --snapshot --target ios
|
|
|
217
272
|
- `prepare` runs in `bootstrap` mode for first-time generation and `update` mode after a snapshot exists
|
|
218
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.
|
|
219
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.
|
package/docs/file-formats.md
CHANGED
|
@@ -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"
|