openuispec 0.1.16 → 0.1.18

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 CHANGED
@@ -70,7 +70,8 @@ openuispec/
70
70
  │ ├── flow.schema.json # Navigation flow schema
71
71
  │ ├── platform.schema.json # Platform adaptation schema
72
72
  │ ├── locale.schema.json # Locale file schema
73
- │ ├── custom-contract.schema.json # Custom contract extension schema
73
+ │ ├── contract.schema.json # Standard contract extension schema
74
+ │ ├── custom-contract.schema.json # Custom contract extension schema (x_ prefixed)
74
75
  │ ├── tokens/
75
76
  │ │ ├── color.schema.json # Color token schema
76
77
  │ │ ├── typography.schema.json # Typography token schema
@@ -99,7 +100,8 @@ openuispec/
99
100
  │ │ ├── layout.yaml # Size classes, primitives, reflow rules
100
101
  │ │ ├── themes.yaml # Light, dark, warm variants
101
102
  │ │ └── icons.yaml # Icon registry with platform mappings
102
- │ ├── contracts/ # 7 contract family stubs + custom contracts
103
+ │ ├── contracts/ # Standard contract extensions + custom contracts
104
+ │ │ ├── input_field.yaml # Standard contract with cut_corner variant
103
105
  │ │ └── x_media_player.yaml # Custom media player contract (Section 12)
104
106
  │ ├── screens/
105
107
  │ │ ├── home.yaml # Task list with search, filters, FAB, adaptive nav
@@ -138,7 +140,8 @@ Every file type has a corresponding JSON Schema in `schema/`. **Read the schema
138
140
  | `flows/*.yaml` | `flow.schema.json` | `<flow_id>` | [create_task.yaml](./examples/taskflow/flows/create_task.yaml) |
139
141
  | `platform/*.yaml` | `platform.schema.json` | `platform` | [ios.yaml](./examples/taskflow/platform/ios.yaml) |
140
142
  | `locales/*.json` | `locale.schema.json` | (object) | [en.json](./examples/taskflow/locales/en.json) |
141
- | `contracts/x_*.yaml` | `custom-contract.schema.json` | `contract` | [x_media_player.yaml](./examples/taskflow/contracts/x_media_player.yaml) |
143
+ | `contracts/<name>.yaml` | `contract.schema.json` | `<contract_name>` | [input_field.yaml](./examples/taskflow/contracts/input_field.yaml) |
144
+ | `contracts/x_*.yaml` | `custom-contract.schema.json` | `<x_name>` | [x_media_player.yaml](./examples/taskflow/contracts/x_media_player.yaml) |
142
145
  | `tokens/color.yaml` | `tokens/color.schema.json` | `color` | [color.yaml](./examples/taskflow/tokens/color.yaml) |
143
146
  | `tokens/typography.yaml` | `tokens/typography.schema.json` | `typography` | [typography.yaml](./examples/taskflow/tokens/typography.yaml) |
144
147
  | `tokens/spacing.yaml` | `tokens/spacing.schema.json` | `spacing` | [spacing.yaml](./examples/taskflow/tokens/spacing.yaml) |
package/cli/init.ts CHANGED
@@ -137,7 +137,7 @@ OpenUISpec is a YAML-based format that describes your app's UI semantically —
137
137
  | \`tokens/\` | Design tokens — colors, typography, spacing, elevation, motion, icons, themes |
138
138
  | \`screens/\` | Screen definitions — one YAML file per screen |
139
139
  | \`flows/\` | Navigation flows — multi-step user journeys |
140
- | \`contracts/\` | Component contracts — custom UI component definitions (\`x_\` prefixed) |
140
+ | \`contracts/\` | Component contracts — standard extensions (variants, tokens) and custom (\`x_\` prefixed) |
141
141
  | \`platform/\` | Platform overrides — per-target (iOS, Android, Web) behaviors |
142
142
  | \`locales/\` | Localization — i18n strings (JSON, ICU MessageFormat) |
143
143
 
@@ -222,7 +222,8 @@ Root keys: \`color\`, \`typography\`, \`spacing\`, \`elevation\`, \`motion\`, \`
222
222
  | \`flows/*.yaml\` | \`flow.schema.json\` | \`<flow_id>\` |
223
223
  | \`platform/*.yaml\` | \`platform.schema.json\` | \`platform\` |
224
224
  | \`locales/*.json\` | \`locale.schema.json\` | (object) |
225
- | \`contracts/x_*.yaml\` | \`custom-contract.schema.json\` | \`contract\` |
225
+ | \`contracts/<name>.yaml\` | \`contract.schema.json\` | \`<contract_name>\` |
226
+ | \`contracts/x_*.yaml\` | \`custom-contract.schema.json\` | \`<x_name>\` |
226
227
  | \`tokens/color.yaml\` | \`tokens/color.schema.json\` | \`color\` |
227
228
  | \`tokens/typography.yaml\` | \`tokens/typography.schema.json\` | \`typography\` |
228
229
  | \`tokens/spacing.yaml\` | \`tokens/spacing.schema.json\` | \`spacing\` |
@@ -300,7 +301,7 @@ OpenUISpec is a YAML-based spec format that describes an app's UI semantically
300
301
  - Tokens: \`${specDir}/tokens/\` — colors, typography, spacing, motion, icons, themes
301
302
  - Screens: \`${specDir}/screens/\` — one YAML file per screen
302
303
  - Flows: \`${specDir}/flows/\` — multi-step navigation journeys
303
- - Contracts: \`${specDir}/contracts/\` — UI component definitions
304
+ - Contracts: \`${specDir}/contracts/\` — standard extensions (variants, tokens) and custom (\`x_\` prefixed)
304
305
  - Platform: \`${specDir}/platform/\` — per-target overrides (iOS, Android, Web)
305
306
  - Locales: \`${specDir}/locales/\` — i18n strings (JSON, ICU MessageFormat)
306
307
 
@@ -373,7 +374,8 @@ Before creating or editing any spec file, read the corresponding JSON Schema. Do
373
374
  | \`flows/*.yaml\` | \`flow.schema.json\` | \`<flow_id>\` |
374
375
  | \`platform/*.yaml\` | \`platform.schema.json\` | \`platform\` |
375
376
  | \`locales/*.json\` | \`locale.schema.json\` | (object) |
376
- | \`contracts/x_*.yaml\` | \`custom-contract.schema.json\` | \`contract\` |
377
+ | \`contracts/<name>.yaml\` | \`contract.schema.json\` | \`<contract_name>\` |
378
+ | \`contracts/x_*.yaml\` | \`custom-contract.schema.json\` | \`<x_name>\` |
377
379
  | \`tokens/color.yaml\` | \`tokens/color.schema.json\` | \`color\` |
378
380
  | \`tokens/typography.yaml\` | \`tokens/typography.schema.json\` | \`typography\` |
379
381
  | \`tokens/spacing.yaml\` | \`tokens/spacing.schema.json\` | \`spacing\` |
@@ -1,7 +1,4 @@
1
- # action_trigger contract — see spec Section 4 for full definition
2
- # This file serves as a machine-readable reference for AI generators.
3
- # The canonical definition lives in spec/openuispec-v0.1.md.
1
+ # action_trigger contract extension
2
+ # Base definition: spec Section 4.1
4
3
 
5
- contract: action_trigger
6
- spec_section: "4"
7
- source: "spec/openuispec-v0.1.md"
4
+ action_trigger: {}
@@ -1,7 +1,4 @@
1
- # collection contract — see spec Section 4 for full definition
2
- # This file serves as a machine-readable reference for AI generators.
3
- # The canonical definition lives in spec/openuispec-v0.1.md.
1
+ # collection contract extension
2
+ # Base definition: spec Section 4.7
4
3
 
5
- contract: collection
6
- spec_section: "4"
7
- source: "spec/openuispec-v0.1.md"
4
+ collection: {}
@@ -1,7 +1,4 @@
1
- # data_display contract — see spec Section 4 for full definition
2
- # This file serves as a machine-readable reference for AI generators.
3
- # The canonical definition lives in spec/openuispec-v0.1.md.
1
+ # data_display contract extension
2
+ # Base definition: spec Section 4.2
4
3
 
5
- contract: data_display
6
- spec_section: "4"
7
- source: "spec/openuispec-v0.1.md"
4
+ data_display: {}
@@ -1,7 +1,4 @@
1
- # feedback contract — see spec Section 4 for full definition
2
- # This file serves as a machine-readable reference for AI generators.
3
- # The canonical definition lives in spec/openuispec-v0.1.md.
1
+ # feedback contract extension
2
+ # Base definition: spec Section 4.5
4
3
 
5
- contract: feedback
6
- spec_section: "4"
7
- source: "spec/openuispec-v0.1.md"
4
+ feedback: {}
@@ -1,7 +1,23 @@
1
- # input_field contract — see spec Section 4 for full definition
2
- # This file serves as a machine-readable reference for AI generators.
3
- # The canonical definition lives in spec/openuispec-v0.1.md.
1
+ # input_field contract extension
2
+ # Base definition: spec Section 4.3
3
+ # Add project-specific variants, token overrides, and generation hints.
4
4
 
5
- contract: input_field
6
- spec_section: "4"
7
- source: "spec/openuispec-v0.1.md"
5
+ input_field:
6
+ variants:
7
+ cut_corner:
8
+ semantic: "Angled corner input for branded forms"
9
+ tokens:
10
+ cut_size: "spacing.sm"
11
+ border: { color: "color.semantic.border", width: 1 }
12
+ background: "color.semantic.surface"
13
+ platform_mapping:
14
+ ios: { shape: "CutCornerShape", clip: true }
15
+ android: { shape: "CutCornerShape" }
16
+ web: { style: "clip-path" }
17
+ generation:
18
+ must_handle:
19
+ - "Cut top-right and bottom-left corners by cut_size"
20
+ - "Maintain focus ring that follows the cut shape"
21
+ - "Placeholder text must remain readable against background"
22
+ should_handle:
23
+ - "Animate corner cut on focus"
@@ -1,7 +1,4 @@
1
- # nav_container contract — see spec Section 4 for full definition
2
- # This file serves as a machine-readable reference for AI generators.
3
- # The canonical definition lives in spec/openuispec-v0.1.md.
1
+ # nav_container contract extension
2
+ # Base definition: spec Section 4.4
4
3
 
5
- contract: nav_container
6
- spec_section: "4"
7
- source: "spec/openuispec-v0.1.md"
4
+ nav_container: {}
@@ -1,7 +1,4 @@
1
- # surface contract — see spec Section 4 for full definition
2
- # This file serves as a machine-readable reference for AI generators.
3
- # The canonical definition lives in spec/openuispec-v0.1.md.
1
+ # surface contract extension
2
+ # Base definition: spec Section 4.6
4
3
 
5
- contract: surface
6
- spec_section: "4"
7
- source: "spec/openuispec-v0.1.md"
4
+ surface: {}
@@ -23,6 +23,8 @@ profile_edit:
23
23
  - contract: data_display
24
24
  variant: inline
25
25
  props:
26
+ title: "user.name"
27
+ subtitle: "user.email"
26
28
  leading:
27
29
  media: "user.avatar"
28
30
  fallback: { initials: "user.name", background: "color.brand.primary" }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openuispec",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "A semantic UI specification format for AI-native, platform-native app development",
@@ -0,0 +1,129 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://openuispec.org/schema/contract.schema.json",
4
+ "title": "OpenUISpec Standard Contract Extension",
5
+ "description": "Extension file for a standard contract — add variants, override tokens, platform mapping, and generation hints",
6
+ "type": "object",
7
+ "minProperties": 1,
8
+ "maxProperties": 1,
9
+ "propertyNames": {
10
+ "enum": [
11
+ "action_trigger",
12
+ "data_display",
13
+ "input_field",
14
+ "nav_container",
15
+ "feedback",
16
+ "surface",
17
+ "collection"
18
+ ]
19
+ },
20
+ "additionalProperties": {
21
+ "$ref": "#/$defs/contract_extension"
22
+ },
23
+ "$defs": {
24
+ "contract_extension": {
25
+ "type": "object",
26
+ "description": "Extension definition — all fields are optional, they add to or override the spec-defined contract",
27
+ "properties": {
28
+ "variants": {
29
+ "type": "object",
30
+ "description": "Named style/behavior variants (e.g. cut_corner, branded, minimal)",
31
+ "additionalProperties": {
32
+ "$ref": "#/$defs/variant_def"
33
+ }
34
+ },
35
+ "additional_props": {
36
+ "type": "object",
37
+ "description": "Additional props beyond the spec definition",
38
+ "additionalProperties": {
39
+ "$ref": "https://openuispec.org/schema/custom-contract.schema.json#/$defs/prop_def"
40
+ }
41
+ },
42
+ "tokens": {
43
+ "type": "object",
44
+ "description": "Token overrides at the contract level",
45
+ "additionalProperties": true
46
+ },
47
+ "platform_mapping": {
48
+ "type": "object",
49
+ "description": "Platform mapping overrides",
50
+ "properties": {
51
+ "ios": { "type": "object", "additionalProperties": true },
52
+ "android": { "type": "object", "additionalProperties": true },
53
+ "web": { "type": "object", "additionalProperties": true }
54
+ },
55
+ "additionalProperties": {
56
+ "type": "object",
57
+ "additionalProperties": true
58
+ }
59
+ },
60
+ "generation": {
61
+ "type": "object",
62
+ "description": "AI generation compliance hints (merged with spec defaults)",
63
+ "properties": {
64
+ "must_handle": {
65
+ "type": "array",
66
+ "items": { "type": "string" }
67
+ },
68
+ "should_handle": {
69
+ "type": "array",
70
+ "items": { "type": "string" }
71
+ },
72
+ "may_handle": {
73
+ "type": "array",
74
+ "items": { "type": "string" }
75
+ }
76
+ },
77
+ "additionalProperties": false
78
+ },
79
+ "test_cases": {
80
+ "type": "array",
81
+ "description": "Additional behavioral test cases",
82
+ "items": {
83
+ "$ref": "https://openuispec.org/schema/custom-contract.schema.json#/$defs/test_case"
84
+ }
85
+ }
86
+ },
87
+ "additionalProperties": false
88
+ },
89
+ "variant_def": {
90
+ "type": "object",
91
+ "description": "A named variant with semantic description, tokens, platform mapping, and generation hints",
92
+ "properties": {
93
+ "semantic": {
94
+ "type": "string",
95
+ "description": "Human-readable description of this variant"
96
+ },
97
+ "tokens": {
98
+ "type": "object",
99
+ "description": "Visual token bindings for this variant",
100
+ "additionalProperties": true
101
+ },
102
+ "platform_mapping": {
103
+ "type": "object",
104
+ "description": "Per-platform implementation hints for this variant",
105
+ "properties": {
106
+ "ios": { "type": "object", "additionalProperties": true },
107
+ "android": { "type": "object", "additionalProperties": true },
108
+ "web": { "type": "object", "additionalProperties": true }
109
+ },
110
+ "additionalProperties": {
111
+ "type": "object",
112
+ "additionalProperties": true
113
+ }
114
+ },
115
+ "generation": {
116
+ "type": "object",
117
+ "description": "Generation hints specific to this variant",
118
+ "properties": {
119
+ "must_handle": { "type": "array", "items": { "type": "string" } },
120
+ "should_handle": { "type": "array", "items": { "type": "string" } },
121
+ "may_handle": { "type": "array", "items": { "type": "string" } }
122
+ },
123
+ "additionalProperties": false
124
+ }
125
+ },
126
+ "additionalProperties": false
127
+ }
128
+ }
129
+ }
@@ -23,6 +23,18 @@ const __dirname = fileURLToPath(new URL(".", import.meta.url));
23
23
  const SCHEMA_DIR = resolve(__dirname);
24
24
 
25
25
  type AjvInstance = InstanceType<typeof Ajv2020>;
26
+ type UnknownRecord = Record<string, unknown>;
27
+
28
+ interface UsageLint {
29
+ path: string;
30
+ message: string;
31
+ }
32
+
33
+ interface StandardContractRule {
34
+ requiredProps?: string[];
35
+ nonEmptyStringProps?: string[];
36
+ validate?: (node: UnknownRecord, props: UnknownRecord, path: string) => UsageLint[];
37
+ }
26
38
 
27
39
  // ── helpers ──────────────────────────────────────────────────────────
28
40
 
@@ -49,6 +61,255 @@ function listFiles(dir: string, ext: string): string[] {
49
61
  }
50
62
  }
51
63
 
64
+ function isRecord(value: unknown): value is UnknownRecord {
65
+ return typeof value === "object" && value !== null && !Array.isArray(value);
66
+ }
67
+
68
+ function isNonEmptyString(value: unknown): value is string {
69
+ return typeof value === "string" && value.trim().length > 0;
70
+ }
71
+
72
+ function getSingleRootValue(data: unknown): unknown {
73
+ if (!isRecord(data)) return undefined;
74
+ const values = Object.values(data);
75
+ return values.length === 1 ? values[0] : undefined;
76
+ }
77
+
78
+ const STANDARD_CONTRACT_RULES: Record<string, StandardContractRule> = {
79
+ action_trigger: {
80
+ requiredProps: ["label"],
81
+ nonEmptyStringProps: ["label"],
82
+ },
83
+ data_display: {
84
+ requiredProps: ["title"],
85
+ nonEmptyStringProps: ["title"],
86
+ },
87
+ input_field: {
88
+ requiredProps: ["label"],
89
+ nonEmptyStringProps: ["label"],
90
+ validate(node, props, path) {
91
+ const inputType = node.input_type;
92
+ if (inputType === "select" || inputType === "radio") {
93
+ return hasOwnProp(props, "options")
94
+ ? []
95
+ : [{
96
+ path,
97
+ message: `contract "input_field" with input_type="${String(inputType)}" requires props.options`,
98
+ }];
99
+ }
100
+ if (inputType === "slider") {
101
+ return hasOwnProp(props, "range")
102
+ ? []
103
+ : [{
104
+ path,
105
+ message: 'contract "input_field" with input_type="slider" requires props.range',
106
+ }];
107
+ }
108
+ return [];
109
+ },
110
+ },
111
+ nav_container: {
112
+ requiredProps: ["items"],
113
+ validate(_node, props, path) {
114
+ const items = props.items;
115
+ if (!Array.isArray(items)) {
116
+ return [];
117
+ }
118
+ const errors: UsageLint[] = [];
119
+ for (const [index, item] of items.entries()) {
120
+ const itemPath = `${path}/props/items/${index}`;
121
+ if (!isRecord(item)) {
122
+ errors.push({
123
+ path: itemPath,
124
+ message: "nav_container items must be objects",
125
+ });
126
+ continue;
127
+ }
128
+ for (const key of ["id", "label", "icon", "destination"]) {
129
+ if (!hasOwnProp(item, key)) {
130
+ errors.push({
131
+ path: itemPath,
132
+ message: `nav_container item requires "${key}"`,
133
+ });
134
+ }
135
+ }
136
+ if (hasOwnProp(item, "label") && !isNonEmptyString(item.label)) {
137
+ errors.push({
138
+ path: `${itemPath}/label`,
139
+ message: 'nav_container item "label" must be a non-empty string',
140
+ });
141
+ }
142
+ }
143
+ return errors;
144
+ },
145
+ },
146
+ feedback: {
147
+ requiredProps: ["message"],
148
+ nonEmptyStringProps: ["message"],
149
+ },
150
+ surface: {
151
+ requiredProps: ["content"],
152
+ },
153
+ collection: {
154
+ requiredProps: ["data", "item_contract", "item_props_map"],
155
+ },
156
+ };
157
+
158
+ function hasOwnProp(obj: UnknownRecord, key: string): boolean {
159
+ return Object.prototype.hasOwnProperty.call(obj, key);
160
+ }
161
+
162
+ function validateStandardContractUsage(
163
+ node: UnknownRecord,
164
+ path: string,
165
+ ): UsageLint[] {
166
+ const contract = node.contract;
167
+ if (typeof contract !== "string") return [];
168
+
169
+ const rule = STANDARD_CONTRACT_RULES[contract];
170
+ if (!rule) return [];
171
+
172
+ const props = isRecord(node.props) ? node.props : {};
173
+ const errors: UsageLint[] = [];
174
+
175
+ for (const prop of rule.requiredProps ?? []) {
176
+ if (!hasOwnProp(props, prop)) {
177
+ errors.push({
178
+ path,
179
+ message: `contract "${contract}" requires props.${prop}`,
180
+ });
181
+ }
182
+ }
183
+
184
+ for (const prop of rule.nonEmptyStringProps ?? []) {
185
+ if (hasOwnProp(props, prop) && !isNonEmptyString(props[prop])) {
186
+ errors.push({
187
+ path: `${path}/props/${prop}`,
188
+ message: `props.${prop} for contract "${contract}" must be a non-empty string`,
189
+ });
190
+ }
191
+ }
192
+
193
+ errors.push(...(rule.validate?.(node, props, path) ?? []));
194
+ return errors;
195
+ }
196
+
197
+ function lintSectionItems(items: unknown, path: string): UsageLint[] {
198
+ if (!Array.isArray(items)) return [];
199
+ const errors: UsageLint[] = [];
200
+
201
+ for (const [index, item] of items.entries()) {
202
+ const itemPath = `${path}/${index}`;
203
+ if (!isRecord(item)) {
204
+ continue;
205
+ }
206
+
207
+ errors.push(...validateStandardContractUsage(item, itemPath));
208
+
209
+ if (Array.isArray(item.children)) {
210
+ errors.push(...lintSectionItems(item.children, `${itemPath}/children`));
211
+ }
212
+ }
213
+
214
+ return errors;
215
+ }
216
+
217
+ function lintScreenLikeDefinition(screenDef: unknown, path: string): UsageLint[] {
218
+ if (!isRecord(screenDef)) return [];
219
+ const errors: UsageLint[] = [];
220
+
221
+ if (isRecord(screenDef.layout)) {
222
+ errors.push(
223
+ ...lintSectionItems(screenDef.layout.sections, `${path}/layout/sections`),
224
+ );
225
+ }
226
+
227
+ if (isRecord(screenDef.navigation)) {
228
+ errors.push(
229
+ ...validateStandardContractUsage(
230
+ screenDef.navigation,
231
+ `${path}/navigation`,
232
+ ),
233
+ );
234
+ }
235
+
236
+ if (isRecord(screenDef.surfaces)) {
237
+ for (const [surfaceId, surfaceDef] of Object.entries(screenDef.surfaces)) {
238
+ const surfacePath = `${path}/surfaces/${surfaceId}`;
239
+ if (!isRecord(surfaceDef)) {
240
+ continue;
241
+ }
242
+
243
+ errors.push(...validateStandardContractUsage(surfaceDef, surfacePath));
244
+
245
+ const props = isRecord(surfaceDef.props) ? surfaceDef.props : {};
246
+ if (Array.isArray(props.content)) {
247
+ errors.push(
248
+ ...lintSectionItems(props.content, `${surfacePath}/props/content`),
249
+ );
250
+ }
251
+ }
252
+ }
253
+
254
+ return errors;
255
+ }
256
+
257
+ function lintScreenFile(dataPath: string): number {
258
+ const root = getSingleRootValue(loadData(dataPath));
259
+ const errors = lintScreenLikeDefinition(root, basename(dataPath));
260
+ if (errors.length === 0) {
261
+ return 0;
262
+ }
263
+
264
+ console.log(` FAIL ${basename(dataPath)} (${errors.length} contract usage error(s))`);
265
+ for (const error of errors.slice(0, 5)) {
266
+ console.log(` [${error.path}] ${error.message}`);
267
+ }
268
+ if (errors.length > 5) {
269
+ console.log(` ... and ${errors.length - 5} more`);
270
+ }
271
+ console.log(
272
+ " Hint: built-in contract instances inherit required props from the spec even when contracts/<name>.yaml does not restate them.",
273
+ );
274
+ return errors.length;
275
+ }
276
+
277
+ function lintFlowFile(dataPath: string): number {
278
+ const root = getSingleRootValue(loadData(dataPath));
279
+ if (!isRecord(root) || !isRecord(root.screens)) {
280
+ return 0;
281
+ }
282
+
283
+ const errors: UsageLint[] = [];
284
+ for (const [screenId, screenEntry] of Object.entries(root.screens)) {
285
+ if (!isRecord(screenEntry) || !isRecord(screenEntry.screen_inline)) {
286
+ continue;
287
+ }
288
+ errors.push(
289
+ ...lintScreenLikeDefinition(
290
+ screenEntry.screen_inline,
291
+ `${basename(dataPath)}/screens/${screenId}/screen_inline`,
292
+ ),
293
+ );
294
+ }
295
+
296
+ if (errors.length === 0) {
297
+ return 0;
298
+ }
299
+
300
+ console.log(` FAIL ${basename(dataPath)} (${errors.length} contract usage error(s))`);
301
+ for (const error of errors.slice(0, 5)) {
302
+ console.log(` [${error.path}] ${error.message}`);
303
+ }
304
+ if (errors.length > 5) {
305
+ console.log(` ... and ${errors.length - 5} more`);
306
+ }
307
+ console.log(
308
+ " Hint: flow screen_inline sections follow the same built-in contract requirements as screens/*.yaml.",
309
+ );
310
+ return errors.length;
311
+ }
312
+
52
313
  // ── build Ajv instance with all schemas ──────────────────────────────
53
314
 
54
315
  function buildAjv(): AjvInstance {
@@ -233,7 +494,11 @@ const GROUPS: Record<string, ValidationGroup> = {
233
494
  let errors = 0;
234
495
  const dir = resolveInclude(projectDir, includes.screens);
235
496
  for (const f of listFiles(dir, ".yaml")) {
236
- errors += validateFile(ajv, f, `${BASE}screen.schema.json`);
497
+ const schemaErrors = validateFile(ajv, f, `${BASE}screen.schema.json`);
498
+ errors += schemaErrors;
499
+ if (schemaErrors === 0) {
500
+ errors += lintScreenFile(f);
501
+ }
237
502
  }
238
503
  return errors;
239
504
  },
@@ -245,7 +510,11 @@ const GROUPS: Record<string, ValidationGroup> = {
245
510
  let errors = 0;
246
511
  const dir = resolveInclude(projectDir, includes.flows);
247
512
  for (const f of listFiles(dir, ".yaml")) {
248
- errors += validateFile(ajv, f, `${BASE}flow.schema.json`);
513
+ const schemaErrors = validateFile(ajv, f, `${BASE}flow.schema.json`);
514
+ errors += schemaErrors;
515
+ if (schemaErrors === 0) {
516
+ errors += lintFlowFile(f);
517
+ }
249
518
  }
250
519
  return errors;
251
520
  },
@@ -275,18 +544,17 @@ const GROUPS: Record<string, ValidationGroup> = {
275
544
  },
276
545
  },
277
546
 
278
- custom_contracts: {
279
- label: "Custom contracts",
547
+ contracts: {
548
+ label: "Contracts",
280
549
  run(ajv, projectDir, includes) {
281
550
  let errors = 0;
282
551
  const dir = resolveInclude(projectDir, includes.contracts);
283
552
  for (const f of listFiles(dir, ".yaml")) {
284
- if (basename(f).startsWith("x_")) {
285
- errors += validateFile(
286
- ajv,
287
- f,
288
- `${BASE}custom-contract.schema.json`,
289
- );
553
+ const name = basename(f);
554
+ if (name.startsWith("x_")) {
555
+ errors += validateFile(ajv, f, `${BASE}custom-contract.schema.json`);
556
+ } else {
557
+ errors += validateFile(ajv, f, `${BASE}contract.schema.json`);
290
558
  }
291
559
  }
292
560
  return errors;
@@ -1392,6 +1392,7 @@ collection:
1392
1392
  grid: "grid"
1393
1393
  table: "table"
1394
1394
  carousel: "region"
1395
+ label: "Derived from visible header text or surrounding section heading; bind with aria-labelledby or platform equivalent rather than a dedicated prop when possible"
1395
1396
  item_role:
1396
1397
  list: "listitem"
1397
1398
  grid: "gridcell"
@@ -1469,6 +1470,8 @@ collection:
1469
1470
  - "Item recycling / virtualization for large datasets"
1470
1471
  ```
1471
1472
 
1473
+ Collection containers still need an accessible name, but unlike controls such as buttons or fields, that name usually comes from visible context rather than a dedicated `label` prop. Generators SHOULD connect the collection to a visible heading or header component via `aria-labelledby` or the platform equivalent, and only fall back to an explicit synthesized label when no visible label source exists.
1474
+
1472
1475
  ---
1473
1476
 
1474
1477
  ## 5. Screen composition
@@ -2022,7 +2025,7 @@ Every AI generator, regardless of platform target, MUST:
2022
2025
  2. Map every `contract` reference to the correct native widget per `platform_mapping`.
2023
2026
  3. Apply all `tokens` values within their declared `range` constraints.
2024
2027
  4. Implement every `state` declared in each used contract, including transitions.
2025
- 5. Set correct `a11y.role` and `a11y.label` for every component instance.
2028
+ 5. Set correct `a11y.role` and `a11y.label` for every component instance. For contextual containers such as `collection`, derive the label from the visible heading/header via `aria-labelledby` or the platform equivalent when possible instead of requiring a dedicated prop.
2026
2029
  6. Respect `themes` by generating light/dark mode support.
2027
2030
  7. Handle `empty`, `loading`, and `error` states for `collection` contracts.
2028
2031
  8. Wire all `action.navigate` declarations to the platform's navigation system.
@@ -2039,7 +2042,7 @@ A valid OpenUISpec document:
2039
2042
  - Contains a root `openuispec.yaml` manifest with `spec_version`
2040
2043
  - References only defined tokens, contracts, screens, and flows
2041
2044
  - Has no circular `flow` transitions
2042
- - Has every `required: true` prop satisfied in screen compositions
2045
+ - Has every `required: true` prop satisfied in screen compositions, including required props inherited from built-in standard contract families even when `contracts/<name>.yaml` does not restate them
2043
2046
  - Has every `screen.params` satisfied by its callers
2044
2047
 
2045
2048
  ### 8.4 Drift detection