openuispec 0.2.15 → 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.
Files changed (49) hide show
  1. package/README.md +4 -2
  2. package/check/audit.ts +251 -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 +96 -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
package/prepare/index.ts CHANGED
@@ -172,6 +172,17 @@ export interface PrepareResult {
172
172
  shared_layers?: SharedLayerInfo[];
173
173
  bootstrap?: PrepareBootstrapBundle;
174
174
  spec_contents?: SpecFileContent[];
175
+ anti_patterns?: {
176
+ universal: Record<string, string[]>;
177
+ contract_specific: Record<string, string[]>;
178
+ project_specific: string[];
179
+ };
180
+ design_context?: {
181
+ personality?: string;
182
+ complexity: 'restrained' | 'balanced' | 'elaborate';
183
+ audience?: string;
184
+ complexity_rule: string;
185
+ };
175
186
  next_steps: string[];
176
187
  }
177
188
 
@@ -643,9 +654,90 @@ function generationRules(target: string, outputDir: string, manifest: Record<str
643
654
  rules.push(`Target "${target}" scope: ${structure.scope}`);
644
655
  }
645
656
 
657
+ // Include extra_rules from manifest, filtered by target platform tag
658
+ const extraRules: string[] = Array.isArray(manifest.generation?.extra_rules)
659
+ ? manifest.generation.extra_rules.filter((rule: any): rule is string => typeof rule === "string")
660
+ : [];
661
+ for (const rule of extraRules) {
662
+ if (matchesTargetPlatform(rule, target)) rules.push(rule);
663
+ }
664
+
646
665
  return rules;
647
666
  }
648
667
 
668
+ function matchesTargetPlatform(item: string, target: string): boolean {
669
+ const tagMatch = item.match(/^\[([a-z]+)\]/);
670
+ return !tagMatch || tagMatch[1] === target;
671
+ }
672
+
673
+ function complexityRule(complexity: string): string {
674
+ switch (complexity) {
675
+ case 'restrained':
676
+ return 'Minimal motion (required state transitions only). No decorative shadows. Clean whitespace. Precise token application. No background effects.';
677
+ case 'elaborate':
678
+ return 'Rich animations with staggered reveals. Creative elevation. Platform-specific flourishes.';
679
+ default:
680
+ return 'Apply all motion.patterns. Use elevation tokens fully. Standard state animations.';
681
+ }
682
+ }
683
+
684
+ function buildDesignContext(manifest: Record<string, any>): PrepareResult['design_context'] {
685
+ const design = manifest.design;
686
+ if (!design) return undefined;
687
+ const complexity = (design.complexity as 'restrained' | 'balanced' | 'elaborate') ?? 'balanced';
688
+ return {
689
+ ...(design.personality ? { personality: design.personality } : {}),
690
+ complexity,
691
+ ...(design.audience ? { audience: design.audience } : {}),
692
+ complexity_rule: complexityRule(complexity),
693
+ };
694
+ }
695
+
696
+ function buildAntiPatterns(
697
+ manifest: Record<string, any>,
698
+ projectDir: string,
699
+ target: string
700
+ ): PrepareResult['anti_patterns'] {
701
+ // Universal anti-patterns from generation_guidance
702
+ const universal: Record<string, string[]> = {};
703
+ const universalRaw = manifest.generation_guidance?.universal_anti_patterns ?? {};
704
+ for (const [domain, items] of Object.entries(universalRaw)) {
705
+ if (Array.isArray(items)) {
706
+ const filtered = (items as string[]).filter((item) => matchesTargetPlatform(item, target));
707
+ if (filtered.length > 0) universal[domain] = filtered;
708
+ }
709
+ }
710
+
711
+ // Contract-specific must_avoid
712
+ 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_'))) {
717
+ const content = YAML.parse(readFileSync(join(contractsDir, file), 'utf-8'));
718
+ const contractName = Object.keys(content)[0];
719
+ const mustAvoid: string[] = content[contractName]?.generation?.must_avoid ?? [];
720
+ if (mustAvoid.length > 0) {
721
+ const filtered = mustAvoid.filter((item: string) => matchesTargetPlatform(item, target));
722
+ if (filtered.length > 0) contract_specific[contractName] = filtered;
723
+ }
724
+ }
725
+ }
726
+ } catch { /* skip on error */ }
727
+
728
+ // Project-specific avoid from design section
729
+ const project_specific: string[] = [];
730
+ const designAvoid: string[] = manifest.design?.avoid ?? [];
731
+ for (const item of designAvoid) {
732
+ if (matchesTargetPlatform(item, target)) project_specific.push(item);
733
+ }
734
+
735
+ const hasContent = Object.keys(universal).length > 0 || Object.keys(contract_specific).length > 0 || project_specific.length > 0;
736
+ if (!hasContent) return undefined;
737
+
738
+ return { universal, contract_specific, project_specific };
739
+ }
740
+
649
741
  function localizationConstraints(
650
742
  target: string,
651
743
  platformConfig?: Pick<PreparePlatformConfig, "framework">
@@ -1228,6 +1320,8 @@ function buildBootstrapPrepareResult(cwd: string, target: string, includeContent
1228
1320
  const outputDirExists = existsSync(outputDir);
1229
1321
  const snapshotPath = join(outputDir, ".openuispec-state.json");
1230
1322
  const snapshotFileExists = existsSync(snapshotPath);
1323
+ const antiPatterns = buildAntiPatterns(manifest, projectDir, target);
1324
+ const designContext = buildDesignContext(manifest);
1231
1325
 
1232
1326
  return {
1233
1327
  mode: "bootstrap",
@@ -1259,6 +1353,8 @@ function buildBootstrapPrepareResult(cwd: string, target: string, includeContent
1259
1353
  items: [],
1260
1354
  ...(sharedLayerInfos.length > 0 ? { shared_layers: sharedLayerInfos } : {}),
1261
1355
  ...(includeContents ? { spec_contents: readAllSpecContents(projectDir) } : {}),
1356
+ ...(antiPatterns ? { anti_patterns: antiPatterns } : {}),
1357
+ ...(designContext ? { design_context: designContext } : {}),
1262
1358
  bootstrap: {
1263
1359
  output_exists: existsSync(outputDir),
1264
1360
  generation_ready: missingDecisions.length === 0 && backendContextReady && !pendingUserConfirmation,
@@ -107,6 +107,11 @@
107
107
  "may_handle": {
108
108
  "type": "array",
109
109
  "items": { "type": "string" }
110
+ },
111
+ "must_avoid": {
112
+ "type": "array",
113
+ "items": { "type": "string" },
114
+ "description": "Anti-patterns for AI generators. Strings may start with [web], [ios], or [android] to scope to a platform. Unmarked items apply to all platforms."
110
115
  }
111
116
  },
112
117
  "additionalProperties": false
@@ -71,6 +71,11 @@
71
71
  "may_handle": {
72
72
  "type": "array",
73
73
  "items": { "type": "string" }
74
+ },
75
+ "must_avoid": {
76
+ "type": "array",
77
+ "items": { "type": "string" },
78
+ "description": "Anti-patterns for AI generators. Strings may start with [web], [ios], or [android] to scope to a platform. Unmarked items apply to all platforms."
74
79
  }
75
80
  },
76
81
  "additionalProperties": false
@@ -116,7 +121,12 @@
116
121
  "properties": {
117
122
  "must_handle": { "type": "array", "items": { "type": "string" } },
118
123
  "should_handle": { "type": "array", "items": { "type": "string" } },
119
- "may_handle": { "type": "array", "items": { "type": "string" } }
124
+ "may_handle": { "type": "array", "items": { "type": "string" } },
125
+ "must_avoid": {
126
+ "type": "array",
127
+ "items": { "type": "string" },
128
+ "description": "Anti-patterns for AI generators. Strings may start with [web], [ios], or [android] to scope to a platform. Unmarked items apply to all platforms."
129
+ }
120
130
  },
121
131
  "additionalProperties": false
122
132
  }
@@ -112,6 +112,11 @@
112
112
  "type": "array",
113
113
  "items": { "type": "string" },
114
114
  "description": "Optional enhancements the generator MAY implement"
115
+ },
116
+ "must_avoid": {
117
+ "type": "array",
118
+ "items": { "type": "string" },
119
+ "description": "Anti-patterns for AI generators. Strings may start with [web], [ios], or [android] to scope to a platform. Unmarked items apply to all platforms."
115
120
  }
116
121
  },
117
122
  "additionalProperties": false
@@ -259,6 +259,53 @@
259
259
  "type": "string"
260
260
  }
261
261
  },
262
+ "design": {
263
+ "type": "object",
264
+ "description": "Design intent and anti-patterns for this project",
265
+ "properties": {
266
+ "personality": { "type": "string", "description": "Brief description of the brand's visual personality" },
267
+ "complexity": {
268
+ "type": "string",
269
+ "enum": ["restrained", "balanced", "elaborate"],
270
+ "default": "balanced",
271
+ "description": "How elaborate animations, effects, and visual details should be"
272
+ },
273
+ "audience": { "type": "string", "description": "Who uses this app — informs tone and complexity" },
274
+ "avoid": {
275
+ "type": "array",
276
+ "items": { "type": "string" },
277
+ "description": "Project-specific anti-patterns. May use [web]/[ios]/[android] scope tags."
278
+ }
279
+ },
280
+ "additionalProperties": false
281
+ },
282
+ "generation_guidance": {
283
+ "type": "object",
284
+ "description": "Universal anti-patterns and audit config for AI generators",
285
+ "properties": {
286
+ "universal_anti_patterns": {
287
+ "type": "object",
288
+ "description": "Cross-contract anti-patterns by domain",
289
+ "properties": {
290
+ "typography": { "type": "array", "items": { "type": "string" } },
291
+ "color": { "type": "array", "items": { "type": "string" } },
292
+ "spacing": { "type": "array", "items": { "type": "string" } },
293
+ "motion": { "type": "array", "items": { "type": "string" } },
294
+ "elevation": { "type": "array", "items": { "type": "string" } },
295
+ "layout": { "type": "array", "items": { "type": "string" } },
296
+ "accessibility": { "type": "array", "items": { "type": "string" } }
297
+ },
298
+ "additionalProperties": false
299
+ },
300
+ "audit_threshold": {
301
+ "type": "integer",
302
+ "minimum": 0,
303
+ "maximum": 100,
304
+ "description": "Minimum design quality score. Default 0 (informational). --min-score CLI flag takes precedence."
305
+ }
306
+ },
307
+ "additionalProperties": false
308
+ },
262
309
  "api": {
263
310
  "type": "object",
264
311
  "description": "API endpoint definitions",
@@ -75,6 +75,11 @@
75
75
  },
76
76
  "contrast_min": {
77
77
  "type": "number"
78
+ },
79
+ "generation_notes": {
80
+ "type": "array",
81
+ "items": { "type": "string" },
82
+ "description": "Sparse guidance for AI generators. Use [web]/[ios]/[android] tags for platform-specific notes."
78
83
  }
79
84
  },
80
85
  "required": [
@@ -75,6 +75,11 @@
75
75
  "pattern": {
76
76
  "type": "string",
77
77
  "description": "Named animation pattern"
78
+ },
79
+ "generation_notes": {
80
+ "type": "array",
81
+ "items": { "type": "string" },
82
+ "description": "Sparse guidance for AI generators. Use [web]/[ios]/[android] tags for platform-specific notes."
78
83
  }
79
84
  },
80
85
  "additionalProperties": false
@@ -60,6 +60,11 @@
60
60
  }
61
61
  },
62
62
  "additionalProperties": false
63
+ },
64
+ "generation_notes": {
65
+ "type": "array",
66
+ "items": { "type": "string" },
67
+ "description": "Sparse guidance for AI generators. Use [web]/[ios]/[android] tags for platform-specific notes."
63
68
  }
64
69
  },
65
70
  "required": [
@@ -93,6 +98,11 @@
93
98
  "capitalize",
94
99
  "none"
95
100
  ]
101
+ },
102
+ "generation_notes": {
103
+ "type": "array",
104
+ "items": { "type": "string" },
105
+ "description": "Sparse guidance for AI generators. Use [web]/[ios]/[android] tags for platform-specific notes."
96
106
  }
97
107
  },
98
108
  "required": [