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/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,92 @@ 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]+)\]/i);
670
+ return !tagMatch || tagMatch[1].toLowerCase() === 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
+ const contractsDir = resolve(projectDir, manifest.includes?.contracts ?? './contracts/');
714
+ if (existsSync(contractsDir)) {
715
+ for (const file of readdirSync(contractsDir).filter((f) => f.endsWith('.yaml') && !f.startsWith('x_'))) {
716
+ try {
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
+ } catch {
725
+ continue;
726
+ }
727
+ }
728
+ }
729
+
730
+ // Project-specific avoid from design section
731
+ const project_specific: string[] = [];
732
+ const designAvoid: string[] = manifest.design?.avoid ?? [];
733
+ for (const item of designAvoid) {
734
+ if (matchesTargetPlatform(item, target)) project_specific.push(item);
735
+ }
736
+
737
+ const hasContent = Object.keys(universal).length > 0 || Object.keys(contract_specific).length > 0 || project_specific.length > 0;
738
+ if (!hasContent) return undefined;
739
+
740
+ return { universal, contract_specific, project_specific };
741
+ }
742
+
649
743
  function localizationConstraints(
650
744
  target: string,
651
745
  platformConfig?: Pick<PreparePlatformConfig, "framework">
@@ -1228,6 +1322,8 @@ function buildBootstrapPrepareResult(cwd: string, target: string, includeContent
1228
1322
  const outputDirExists = existsSync(outputDir);
1229
1323
  const snapshotPath = join(outputDir, ".openuispec-state.json");
1230
1324
  const snapshotFileExists = existsSync(snapshotPath);
1325
+ const antiPatterns = buildAntiPatterns(manifest, projectDir, target);
1326
+ const designContext = buildDesignContext(manifest);
1231
1327
 
1232
1328
  return {
1233
1329
  mode: "bootstrap",
@@ -1259,6 +1355,8 @@ function buildBootstrapPrepareResult(cwd: string, target: string, includeContent
1259
1355
  items: [],
1260
1356
  ...(sharedLayerInfos.length > 0 ? { shared_layers: sharedLayerInfos } : {}),
1261
1357
  ...(includeContents ? { spec_contents: readAllSpecContents(projectDir) } : {}),
1358
+ ...(antiPatterns ? { anti_patterns: antiPatterns } : {}),
1359
+ ...(designContext ? { design_context: designContext } : {}),
1262
1360
  bootstrap: {
1263
1361
  output_exists: existsSync(outputDir),
1264
1362
  generation_ready: missingDecisions.length === 0 && backendContextReady && !pendingUserConfirmation,
@@ -1290,6 +1388,8 @@ function buildUpdatePrepareResult(cwd: string, target: string, includeContents:
1290
1388
  const sharedLayerConfigs = sharedLayersForTarget(projectDir, target);
1291
1389
  const codeRoots = suggestCodeRoots(target, outputDir, projectDir, sharedLayerConfigs);
1292
1390
  const manifest = readManifest(projectDir);
1391
+ const antiPatterns = buildAntiPatterns(manifest, projectDir, target);
1392
+ const designContext = buildDesignContext(manifest);
1293
1393
  const sharedLayerInfos = buildSharedLayerInfos(projectDir, target, sharedLayerConfigs);
1294
1394
  const platformDef = readPlatformDefinition(projectDir, manifest, target);
1295
1395
  const platformConfig = buildPlatformConfig(target, platformDef);
@@ -1333,6 +1433,8 @@ function buildUpdatePrepareResult(cwd: string, target: string, includeContents:
1333
1433
  items,
1334
1434
  ...(sharedLayerInfos.length > 0 ? { shared_layers: sharedLayerInfos } : {}),
1335
1435
  ...(includeContents ? { spec_contents: readAllSpecContents(projectDir) } : {}),
1436
+ ...(antiPatterns ? { anti_patterns: antiPatterns } : {}),
1437
+ ...(designContext ? { design_context: designContext } : {}),
1336
1438
  next_steps: nextSteps,
1337
1439
  };
1338
1440
  }
@@ -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": [
@@ -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 };