openuispec 0.1.45 → 0.1.47

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 (138) hide show
  1. package/README.md +17 -5
  2. package/cli/init.ts +21 -3
  3. package/examples/social-app/.mcp.json +10 -0
  4. package/examples/social-app/AGENTS.md +114 -0
  5. package/examples/social-app/CLAUDE.md +114 -0
  6. package/examples/social-app/README.md +19 -0
  7. package/examples/social-app/backend/.gitkeep +1 -0
  8. package/examples/social-app/generated/android/social-app/app/build.gradle.kts +92 -0
  9. package/examples/social-app/generated/android/social-app/app/src/main/AndroidManifest.xml +26 -0
  10. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/AppContainer.kt +20 -0
  11. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/MainActivity.kt +35 -0
  12. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/SocialAppApplication.kt +13 -0
  13. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/data/MockData.kt +98 -0
  14. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/data/preferences/AppPreferences.kt +19 -0
  15. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/data/preferences/DataStorePreferencesRepository.kt +68 -0
  16. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/data/preferences/PreferencesRepository.kt +15 -0
  17. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/model/Models.kt +34 -0
  18. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/MainShell.kt +390 -0
  19. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/components/Components.kt +234 -0
  20. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/components/ContractPrimitives.kt +641 -0
  21. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/navigation/RootComponent.kt +113 -0
  22. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/ChatDetailScreen.kt +212 -0
  23. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/CreatePostScreen.kt +113 -0
  24. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/DiscoverScreen.kt +137 -0
  25. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/EditProfileScreen.kt +180 -0
  26. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/HomeFeedScreen.kt +157 -0
  27. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/MessagesInboxScreen.kt +85 -0
  28. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/NotificationsScreen.kt +74 -0
  29. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/PostDetailScreen.kt +293 -0
  30. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/ProfileSelfScreen.kt +116 -0
  31. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/SearchResultsScreen.kt +161 -0
  32. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/SettingsScreen.kt +164 -0
  33. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/SettingsStore.kt +95 -0
  34. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/UserProfileScreen.kt +123 -0
  35. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/theme/Color.kt +33 -0
  36. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/theme/Shape.kt +41 -0
  37. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/theme/Spacing.kt +20 -0
  38. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/theme/Theme.kt +82 -0
  39. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/theme/Type.kt +60 -0
  40. package/examples/social-app/generated/android/social-app/app/src/main/res/drawable/ic_launcher_foreground.xml +9 -0
  41. package/examples/social-app/generated/android/social-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +5 -0
  42. package/examples/social-app/generated/android/social-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +5 -0
  43. package/examples/social-app/generated/android/social-app/app/src/main/res/values/strings.xml +91 -0
  44. package/examples/social-app/generated/android/social-app/app/src/main/res/values/themes.xml +10 -0
  45. package/examples/social-app/generated/android/social-app/app/src/main/res/values-ru/strings.xml +79 -0
  46. package/examples/social-app/generated/android/social-app/app/src/main/res/values-uz/strings.xml +79 -0
  47. package/examples/social-app/generated/android/social-app/app/src/main/xml/AndroidManifest.xml +23 -0
  48. package/examples/social-app/generated/android/social-app/build.gradle.kts +6 -0
  49. package/examples/social-app/generated/android/social-app/gradle/libs.versions.toml +48 -0
  50. package/examples/social-app/generated/android/social-app/gradle/wrapper/gradle-wrapper.properties +8 -0
  51. package/examples/social-app/generated/android/social-app/gradle.properties +11 -0
  52. package/examples/social-app/generated/android/social-app/gradlew +25 -0
  53. package/examples/social-app/generated/android/social-app/settings.gradle.kts +23 -0
  54. package/examples/social-app/generated/web/social-app/index.html +12 -0
  55. package/examples/social-app/generated/web/social-app/package-lock.json +2517 -0
  56. package/examples/social-app/generated/web/social-app/package.json +27 -0
  57. package/examples/social-app/generated/web/social-app/src/app/App.tsx +58 -0
  58. package/examples/social-app/generated/web/social-app/src/components/Shell.tsx +259 -0
  59. package/examples/social-app/generated/web/social-app/src/components/cards.tsx +317 -0
  60. package/examples/social-app/generated/web/social-app/src/components/ui.tsx +340 -0
  61. package/examples/social-app/generated/web/social-app/src/flows/CreatePostFlow.tsx +86 -0
  62. package/examples/social-app/generated/web/social-app/src/i18n.tsx +59 -0
  63. package/examples/social-app/generated/web/social-app/src/lib/icons.tsx +85 -0
  64. package/examples/social-app/generated/web/social-app/src/lib/tokens.ts +70 -0
  65. package/examples/social-app/generated/web/social-app/src/lib/utils.ts +97 -0
  66. package/examples/social-app/generated/web/social-app/src/locales/en.json +67 -0
  67. package/examples/social-app/generated/web/social-app/src/locales/ru.json +67 -0
  68. package/examples/social-app/generated/web/social-app/src/locales/uz.json +67 -0
  69. package/examples/social-app/generated/web/social-app/src/main.tsx +16 -0
  70. package/examples/social-app/generated/web/social-app/src/screens/ChatDetailScreen.tsx +90 -0
  71. package/examples/social-app/generated/web/social-app/src/screens/DiscoverScreen.tsx +86 -0
  72. package/examples/social-app/generated/web/social-app/src/screens/EditProfileScreen.tsx +57 -0
  73. package/examples/social-app/generated/web/social-app/src/screens/HomeFeedScreen.tsx +103 -0
  74. package/examples/social-app/generated/web/social-app/src/screens/MessagesInboxScreen.tsx +52 -0
  75. package/examples/social-app/generated/web/social-app/src/screens/NotificationsScreen.tsx +41 -0
  76. package/examples/social-app/generated/web/social-app/src/screens/PostDetailScreen.tsx +115 -0
  77. package/examples/social-app/generated/web/social-app/src/screens/ProfileSelfScreen.tsx +57 -0
  78. package/examples/social-app/generated/web/social-app/src/screens/ProfileUserScreen.tsx +76 -0
  79. package/examples/social-app/generated/web/social-app/src/screens/SearchResultsScreen.tsx +96 -0
  80. package/examples/social-app/generated/web/social-app/src/screens/SettingsScreen.tsx +79 -0
  81. package/examples/social-app/generated/web/social-app/src/state/store.ts +592 -0
  82. package/examples/social-app/generated/web/social-app/src/styles.css +124 -0
  83. package/examples/social-app/generated/web/social-app/src/vite-env.d.ts +1 -0
  84. package/examples/social-app/generated/web/social-app/tsconfig.json +22 -0
  85. package/examples/social-app/generated/web/social-app/tsconfig.node.json +13 -0
  86. package/examples/social-app/generated/web/social-app/tsconfig.node.tsbuildinfo +1 -0
  87. package/examples/social-app/generated/web/social-app/tsconfig.tsbuildinfo +1 -0
  88. package/examples/social-app/generated/web/social-app/vite.config.d.ts +2 -0
  89. package/examples/social-app/generated/web/social-app/vite.config.js +6 -0
  90. package/examples/social-app/generated/web/social-app/vite.config.ts +7 -0
  91. package/examples/social-app/openuispec/README.md +56 -0
  92. package/examples/social-app/openuispec/contracts/.gitkeep +0 -0
  93. package/examples/social-app/openuispec/contracts/action_trigger.yaml +104 -0
  94. package/examples/social-app/openuispec/contracts/collection.yaml +43 -0
  95. package/examples/social-app/openuispec/contracts/data_display.yaml +47 -0
  96. package/examples/social-app/openuispec/contracts/feedback.yaml +49 -0
  97. package/examples/social-app/openuispec/contracts/input_field.yaml +41 -0
  98. package/examples/social-app/openuispec/contracts/nav_container.yaml +34 -0
  99. package/examples/social-app/openuispec/contracts/surface.yaml +41 -0
  100. package/examples/social-app/openuispec/flows/.gitkeep +0 -0
  101. package/examples/social-app/openuispec/flows/create_post.yaml +66 -0
  102. package/examples/social-app/openuispec/locales/.gitkeep +0 -0
  103. package/examples/social-app/openuispec/locales/en.json +67 -0
  104. package/examples/social-app/openuispec/locales/ru.json +67 -0
  105. package/examples/social-app/openuispec/locales/uz.json +67 -0
  106. package/examples/social-app/openuispec/openuispec.yaml +214 -0
  107. package/examples/social-app/openuispec/platform/.gitkeep +0 -0
  108. package/examples/social-app/openuispec/platform/android.yaml +30 -0
  109. package/examples/social-app/openuispec/platform/ios.yaml +19 -0
  110. package/examples/social-app/openuispec/platform/web.yaml +23 -0
  111. package/examples/social-app/openuispec/screens/.gitkeep +0 -0
  112. package/examples/social-app/openuispec/screens/chat_detail.yaml +53 -0
  113. package/examples/social-app/openuispec/screens/discover.yaml +78 -0
  114. package/examples/social-app/openuispec/screens/edit_profile.yaml +78 -0
  115. package/examples/social-app/openuispec/screens/home_feed.yaml +138 -0
  116. package/examples/social-app/openuispec/screens/messages_inbox.yaml +43 -0
  117. package/examples/social-app/openuispec/screens/notifications.yaml +29 -0
  118. package/examples/social-app/openuispec/screens/post_detail.yaml +86 -0
  119. package/examples/social-app/openuispec/screens/profile_self.yaml +53 -0
  120. package/examples/social-app/openuispec/screens/profile_user.yaml +60 -0
  121. package/examples/social-app/openuispec/screens/search_results.yaml +62 -0
  122. package/examples/social-app/openuispec/screens/settings.yaml +99 -0
  123. package/examples/social-app/openuispec/tokens/.gitkeep +0 -0
  124. package/examples/social-app/openuispec/tokens/color.yaml +76 -0
  125. package/examples/social-app/openuispec/tokens/elevation.yaml +31 -0
  126. package/examples/social-app/openuispec/tokens/icons.yaml +147 -0
  127. package/examples/social-app/openuispec/tokens/layout.yaml +37 -0
  128. package/examples/social-app/openuispec/tokens/motion.yaml +28 -0
  129. package/examples/social-app/openuispec/tokens/spacing.yaml +19 -0
  130. package/examples/social-app/openuispec/tokens/themes.yaml +31 -0
  131. package/examples/social-app/openuispec/tokens/typography.yaml +50 -0
  132. package/examples/social-app/package.json +12 -0
  133. package/examples/taskflow/openuispec/openuispec.yaml +2 -0
  134. package/examples/todo-orbit/openuispec/openuispec.yaml +2 -0
  135. package/mcp-server/index.ts +200 -10
  136. package/package.json +1 -1
  137. package/schema/openuispec.schema.json +7 -0
  138. package/spec/openuispec-v0.1.md +13 -0
@@ -42,6 +42,12 @@ function getPackageVersion(): string {
42
42
  }
43
43
  }
44
44
 
45
+ // ── spec directory resolver ─────────────────────────────────────────
46
+
47
+ function resolveSpecDir(projectDir: string, manifest: any, key: string): string {
48
+ return resolve(projectDir, manifest.includes?.[key] ?? `./${key}/`);
49
+ }
50
+
45
51
  // ── shared tool helpers ──────────────────────────────────────────────
46
52
 
47
53
  const targetSchema = z.enum(SUPPORTED_TARGETS).describe("Target platform");
@@ -60,7 +66,7 @@ function toolError(err: unknown): { content: [{ type: "text"; text: string }]; i
60
66
 
61
67
  // ── create server ────────────────────────────────────────────────────
62
68
 
63
- const server = new McpServer(
69
+ export const server = new McpServer(
64
70
  {
65
71
  name: "openuispec",
66
72
  version: getPackageVersion(),
@@ -97,6 +103,14 @@ When you need to create or edit spec files and are unsure of the format:
97
103
  2. Call openuispec_spec_schema with the specific type to get the full JSON schema.
98
104
  3. Write the spec file following the schema exactly.
99
105
 
106
+ FOCUSED GETTERS (prefer these for incremental edits over read_specs):
107
+ - openuispec_get_screen(name) — single screen spec
108
+ - openuispec_get_contract(name, variant?) — single contract, optionally one variant
109
+ - openuispec_get_tokens(category) — single token category (color, typography, spacing, etc.)
110
+ - openuispec_get_locale(locale, keys?) — single locale file, optionally filtered keys
111
+ - openuispec_check(target, screens?, contracts?) — scoped audit for specific screens/contracts
112
+ Use read_specs for full-project generation; use focused getters when editing one screen or contract.
113
+
100
114
  Skip these tools ONLY when the request is purely non-UI (API logic, database, infrastructure, etc.)
101
115
  or explicitly platform-specific polish that doesn't affect shared UI semantics.`,
102
116
  }
@@ -121,7 +135,7 @@ server.registerTool(
121
135
 
122
136
  // ── tool: openuispec_check ───────────────────────────────────────────
123
137
 
124
- function buildAuditChecklist(projectDir: string, target: string): string {
138
+ function buildAuditChecklist(projectDir: string, target: string, screenFilter?: string[], contractFilter?: string[]): string {
125
139
  const lines: string[] = [
126
140
  "POST-GENERATION AUDIT — verify your code against these concrete spec requirements:",
127
141
  "",
@@ -133,7 +147,7 @@ function buildAuditChecklist(projectDir: string, target: string): string {
133
147
 
134
148
  // Extract must_handle from contracts
135
149
  const manifest = YAML.parse(fsReadFileSync(join(projectDir, "openuispec.yaml"), "utf-8"));
136
- const contractsDir = resolve(projectDir, manifest.includes?.contracts ?? "./contracts/");
150
+ const contractsDir = resolveSpecDir(projectDir, manifest, "contracts");
137
151
 
138
152
  if (existsSync(contractsDir)) {
139
153
  lines.push("## Contract must_handle requirements");
@@ -141,6 +155,7 @@ function buildAuditChecklist(projectDir: string, target: string): string {
141
155
  try {
142
156
  const content = YAML.parse(fsReadFileSync(join(contractsDir, file), "utf-8"));
143
157
  const contractName = Object.keys(content)[0];
158
+ if (contractFilter && !contractFilter.includes(contractName)) continue;
144
159
  const contract = content[contractName];
145
160
  if (!contract?.variants) continue;
146
161
 
@@ -168,13 +183,14 @@ function buildAuditChecklist(projectDir: string, target: string): string {
168
183
  }
169
184
 
170
185
  // Extract screens and their sections
171
- const screensDir = resolve(projectDir, manifest.includes?.screens ?? "./screens/");
186
+ const screensDir = resolveSpecDir(projectDir, manifest, "screens");
172
187
  if (existsSync(screensDir)) {
173
188
  lines.push("## Screens — verify all sections exist in generated code");
174
189
  for (const file of readdirSync(screensDir).filter(f => f.endsWith(".yaml")).sort()) {
175
190
  try {
176
191
  const content = YAML.parse(fsReadFileSync(join(screensDir, file), "utf-8"));
177
192
  const screenName = Object.keys(content)[0];
193
+ if (screenFilter && !screenFilter.includes(screenName)) continue;
178
194
  const screen = content[screenName];
179
195
  if (screen?.status === "stub") continue;
180
196
 
@@ -212,7 +228,7 @@ function buildAuditChecklist(projectDir: string, target: string): string {
212
228
  }
213
229
 
214
230
  // Locale keys count
215
- const localesDir = resolve(projectDir, manifest.includes?.locales ?? "./locales/");
231
+ const localesDir = resolveSpecDir(projectDir, manifest, "locales");
216
232
  if (existsSync(localesDir)) {
217
233
  const localeFiles = readdirSync(localesDir).filter(f => f.endsWith(".json"));
218
234
  if (localeFiles.length > 0) {
@@ -228,7 +244,7 @@ function buildAuditChecklist(projectDir: string, target: string): string {
228
244
  }
229
245
 
230
246
  // Platform-specific checks
231
- const platformDir = resolve(projectDir, manifest.includes?.platform ?? "./platform/");
247
+ const platformDir = resolveSpecDir(projectDir, manifest, "platform");
232
248
  const platformPath = join(platformDir, `${target}.yaml`);
233
249
  if (existsSync(platformPath)) {
234
250
  try {
@@ -254,14 +270,20 @@ function buildAuditChecklist(projectDir: string, target: string): string {
254
270
  server.registerTool(
255
271
  "openuispec_check",
256
272
  {
257
- description: "Run composite validation + post-generation audit. Returns schema validation results AND a concrete audit checklist derived from your spec files — listing every contract must_handle item, every screen section, and every locale file that must exist in your generated code. Verify each item.",
258
- inputSchema: { target: targetSchema },
273
+ description: "Run composite validation + post-generation audit. Returns schema validation results AND a concrete audit checklist derived from your spec files — listing every contract must_handle item, every screen section, and every locale file that must exist in your generated code. Verify each item. Use optional screens/contracts params to scope the audit to specific items (validation still runs on all files).",
274
+ inputSchema: {
275
+ target: targetSchema,
276
+ screens: z.array(z.string()).optional().describe("Screen names to audit (e.g. ['home_feed', 'settings']). If omitted, audits all screens."),
277
+ contracts: z.array(z.string()).optional().describe("Contract names to audit (e.g. ['action_trigger']). If omitted, audits all contracts."),
278
+ },
259
279
  },
260
- async ({ target }) => {
280
+ async ({ target, screens, contracts }) => {
261
281
  try {
262
282
  const result = buildCheckResult(target, projectCwd);
263
283
  const projectDir = findProjectDir(projectCwd);
264
- const audit = buildAuditChecklist(projectDir, target);
284
+ const screenFilter = screens && screens.length > 0 ? screens : undefined;
285
+ const contractFilter = contracts && contracts.length > 0 ? contracts : undefined;
286
+ const audit = buildAuditChecklist(projectDir, target, screenFilter, contractFilter);
265
287
  return {
266
288
  content: [
267
289
  { type: "text" as const, text: JSON.stringify(result, null, 2) },
@@ -432,6 +454,174 @@ server.registerTool(
432
454
  }
433
455
  );
434
456
 
457
+ // ── tool: openuispec_get_screen ──────────────────────────────────────
458
+
459
+ server.registerTool(
460
+ "openuispec_get_screen",
461
+ {
462
+ description: "Get the parsed content of a single screen spec file. Faster than read_specs when you only need one screen.",
463
+ inputSchema: {
464
+ name: z.string().describe("Screen name, e.g. 'home_feed' (matches filename without .yaml)"),
465
+ },
466
+ },
467
+ async ({ name }) => {
468
+ try {
469
+ const projectDir = findProjectDir(projectCwd);
470
+ const manifest = YAML.parse(fsReadFileSync(join(projectDir, "openuispec.yaml"), "utf-8"));
471
+ const screensDir = resolveSpecDir(projectDir, manifest, "screens");
472
+ const filePath = join(screensDir, `${name}.yaml`);
473
+ if (!existsSync(filePath)) {
474
+ return toolError(`Screen "${name}" not found. Expected file: ${filePath}`);
475
+ }
476
+ const content = fsReadFileSync(filePath, "utf-8");
477
+ return toolResult({ name, path: relative(projectDir, filePath), content });
478
+ } catch (err) {
479
+ return toolError(err);
480
+ }
481
+ }
482
+ );
483
+
484
+ // ── tool: openuispec_get_contract ───────────────────────────────────
485
+
486
+ server.registerTool(
487
+ "openuispec_get_contract",
488
+ {
489
+ description: "Get a single contract spec, optionally filtered to one variant. Faster than read_specs when you only need one contract.",
490
+ inputSchema: {
491
+ name: z.string().describe("Contract name, e.g. 'action_trigger'"),
492
+ variant: z.string().optional().describe("Optional variant name, e.g. 'fab'. If given, returns only that variant's definition."),
493
+ },
494
+ },
495
+ async ({ name, variant }) => {
496
+ try {
497
+ const projectDir = findProjectDir(projectCwd);
498
+ const manifest = YAML.parse(fsReadFileSync(join(projectDir, "openuispec.yaml"), "utf-8"));
499
+ const contractsDir = resolveSpecDir(projectDir, manifest, "contracts");
500
+
501
+ if (!existsSync(contractsDir)) {
502
+ return toolError(`Contracts directory not found: ${contractsDir}`);
503
+ }
504
+
505
+ // Scan contract files for the matching contract key
506
+ for (const file of readdirSync(contractsDir).filter(f => f.endsWith(".yaml")).sort()) {
507
+ const filePath = join(contractsDir, file);
508
+ const raw = fsReadFileSync(filePath, "utf-8");
509
+ const content = YAML.parse(raw);
510
+ const contractName = Object.keys(content)[0];
511
+ if (contractName !== name) continue;
512
+
513
+ if (variant) {
514
+ const contract = content[contractName];
515
+ const variantDef = contract?.variants?.[variant];
516
+ if (!variantDef) {
517
+ return toolError(`Variant "${variant}" not found in contract "${name}". Available variants: ${Object.keys(contract?.variants ?? {}).join(", ")}`);
518
+ }
519
+ return toolResult({ name, variant, definition: variantDef });
520
+ }
521
+
522
+ return toolResult({ name, path: relative(projectDir, filePath), content: raw });
523
+ }
524
+
525
+ return toolError(`Contract "${name}" not found in ${contractsDir}`);
526
+ } catch (err) {
527
+ return toolError(err);
528
+ }
529
+ }
530
+ );
531
+
532
+ // ── tool: openuispec_get_tokens ─────────────────────────────────────
533
+
534
+ server.registerTool(
535
+ "openuispec_get_tokens",
536
+ {
537
+ description: "Get tokens for a specific category (color, typography, spacing, elevation, motion, layout, themes, icons). Faster than read_specs when you only need one token file.",
538
+ inputSchema: {
539
+ category: z.string().describe("Token category, e.g. 'color', 'typography', 'spacing', 'elevation', 'motion', 'layout', 'themes', 'icons'"),
540
+ },
541
+ },
542
+ async ({ category }) => {
543
+ try {
544
+ const projectDir = findProjectDir(projectCwd);
545
+ const manifest = YAML.parse(fsReadFileSync(join(projectDir, "openuispec.yaml"), "utf-8"));
546
+ const tokensDir = resolveSpecDir(projectDir, manifest, "tokens");
547
+
548
+ if (!existsSync(tokensDir)) {
549
+ return toolError(`Tokens directory not found: ${tokensDir}`);
550
+ }
551
+
552
+ // Try exact match first, then scan for files containing the category name
553
+ const candidates = [
554
+ `${category}.yaml`,
555
+ `${category}.yml`,
556
+ ];
557
+
558
+ for (const candidate of candidates) {
559
+ const filePath = join(tokensDir, candidate);
560
+ if (existsSync(filePath)) {
561
+ const content = fsReadFileSync(filePath, "utf-8");
562
+ return toolResult({ category, path: relative(projectDir, filePath), content });
563
+ }
564
+ }
565
+
566
+ // List available token files for helpful error
567
+ const available = readdirSync(tokensDir)
568
+ .filter(f => f.endsWith(".yaml") || f.endsWith(".yml"))
569
+ .map(f => f.replace(/\.ya?ml$/, ""));
570
+ return toolError(`Token category "${category}" not found. Available: ${available.join(", ")}`);
571
+ } catch (err) {
572
+ return toolError(err);
573
+ }
574
+ }
575
+ );
576
+
577
+ // ── tool: openuispec_get_locale ─────────────────────────────────────
578
+
579
+ server.registerTool(
580
+ "openuispec_get_locale",
581
+ {
582
+ description: "Get a single locale file, optionally filtered to specific keys. Faster than read_specs when you only need one locale or specific translation keys.",
583
+ inputSchema: {
584
+ locale: z.string().describe("Locale code, e.g. 'en', 'ru'"),
585
+ keys: z.array(z.string()).optional().describe("Optional list of keys to filter to, e.g. ['nav.home', 'nav.create']. If omitted, returns the full locale file."),
586
+ },
587
+ },
588
+ async ({ locale, keys }) => {
589
+ try {
590
+ const projectDir = findProjectDir(projectCwd);
591
+ const manifest = YAML.parse(fsReadFileSync(join(projectDir, "openuispec.yaml"), "utf-8"));
592
+ const localesDir = resolveSpecDir(projectDir, manifest, "locales");
593
+ const filePath = join(localesDir, `${locale}.json`);
594
+
595
+ if (!existsSync(filePath)) {
596
+ if (existsSync(localesDir)) {
597
+ const available = readdirSync(localesDir)
598
+ .filter(f => f.endsWith(".json"))
599
+ .map(f => f.replace(/\.json$/, ""));
600
+ return toolError(`Locale "${locale}" not found. Available: ${available.join(", ")}`);
601
+ }
602
+ return toolError(`Locales directory not found: ${localesDir}`);
603
+ }
604
+
605
+ const raw = fsReadFileSync(filePath, "utf-8");
606
+ const content = JSON.parse(raw);
607
+
608
+ if (keys && keys.length > 0) {
609
+ const filtered: Record<string, unknown> = {};
610
+ for (const key of keys) {
611
+ if (key in content) {
612
+ filtered[key] = content[key];
613
+ }
614
+ }
615
+ return toolResult({ locale, path: relative(projectDir, filePath), content: filtered });
616
+ }
617
+
618
+ return toolResult({ locale, path: relative(projectDir, filePath), content });
619
+ } catch (err) {
620
+ return toolError(err);
621
+ }
622
+ }
623
+ );
624
+
435
625
  // ── start server ─────────────────────────────────────────────────────
436
626
 
437
627
  export async function startMcpServer() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openuispec",
3
- "version": "0.1.45",
3
+ "version": "0.1.47",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "A semantic UI specification format for AI-native, platform-native app development",
@@ -104,6 +104,13 @@
104
104
  "type": "string"
105
105
  }
106
106
  },
107
+ "extra_rules": {
108
+ "type": "array",
109
+ "description": "Project-wide authoring and generation conventions for AI, including optional scoped hint labels such as [common], [ios], [android], and [web].",
110
+ "items": {
111
+ "type": "string"
112
+ }
113
+ },
107
114
  "output_dir": {
108
115
  "type": "object",
109
116
  "description": "Per-target output directory (relative to openuispec.yaml). Defaults to generated/<target>/<project_name> if not set.",
@@ -83,6 +83,8 @@ includes:
83
83
 
84
84
  generation:
85
85
  targets: [ios, android, web]
86
+ extra_rules:
87
+ - "Generation hint strings may start with [common], [ios], [android], or [web] to indicate scope."
86
88
  ai_model: "any" # no model lock-in
87
89
  output_format:
88
90
  ios: { language: swift, framework: swiftui }
@@ -3248,6 +3250,17 @@ ios:
3248
3250
  - Generate test code based on `test_cases`
3249
3251
  - Add platform-specific enhancements beyond what the contract specifies
3250
3252
 
3253
+ ### Scoped generation hint labels
3254
+
3255
+ Projects may declare authoring conventions in `generation.extra_rules` inside `openuispec.yaml`. One supported convention is prefixing generation hint strings with scope labels such as `[common]`, `[ios]`, `[android]`, and `[web]`. These labels are advisory authoring metadata for humans and AI; they do not change schema semantics unless downstream tooling chooses to interpret them.
3256
+
3257
+ ```yaml
3258
+ generation:
3259
+ targets: [ios, android, web]
3260
+ extra_rules:
3261
+ - "Generation hint strings may start with [common], [ios], [android], or [web] to indicate scope."
3262
+ ```
3263
+
3251
3264
  ### 12.8 Extending standard contracts
3252
3265
 
3253
3266
  The 7 built-in contract families (Section 4) can be extended per-project using `contracts/<name>.yaml` files. Extensions add project-specific **variants**, **token overrides**, **platform mapping**, and **generation hints** without redefining the base contract. The base definition (props, states, a11y) remains authoritative from the spec.