openuispec 0.1.17 → 0.1.19

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 (97) hide show
  1. package/README.md +52 -34
  2. package/cli/index.ts +1 -1
  3. package/docs/stress-test-maturity-report.md +97 -0
  4. package/examples/taskflow/screens/profile_edit.yaml +2 -0
  5. package/examples/todo-orbit/AGENTS.md +127 -0
  6. package/examples/todo-orbit/CLAUDE.md +127 -0
  7. package/examples/todo-orbit/README.md +62 -0
  8. package/examples/todo-orbit/generated/android/Todo Orbit/README.md +14 -0
  9. package/examples/todo-orbit/generated/android/Todo Orbit/app/build.gradle.kts +58 -0
  10. package/examples/todo-orbit/generated/android/Todo Orbit/app/proguard-rules.pro +1 -0
  11. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/AndroidManifest.xml +20 -0
  12. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/MainActivity.kt +14 -0
  13. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/TodoOrbitApp.kt +345 -0
  14. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/support/AppLogic.kt +231 -0
  15. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/support/Models.kt +169 -0
  16. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/support/Strings.kt +8 -0
  17. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/components/CommonComponents.kt +185 -0
  18. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/screens/AnalyticsScreen.kt +193 -0
  19. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/screens/SettingsScreen.kt +102 -0
  20. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/screens/TasksScreen.kt +342 -0
  21. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/sheets/EditorSheets.kt +344 -0
  22. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/theme/TodoOrbitTheme.kt +59 -0
  23. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/res/values/strings.xml +148 -0
  24. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/res/values-ru/strings.xml +154 -0
  25. package/examples/todo-orbit/generated/android/Todo Orbit/build.gradle.kts +4 -0
  26. package/examples/todo-orbit/generated/android/Todo Orbit/gradle/wrapper/gradle-wrapper.jar +0 -0
  27. package/examples/todo-orbit/generated/android/Todo Orbit/gradle/wrapper/gradle-wrapper.properties +7 -0
  28. package/examples/todo-orbit/generated/android/Todo Orbit/gradle.properties +4 -0
  29. package/examples/todo-orbit/generated/android/Todo Orbit/gradlew +248 -0
  30. package/examples/todo-orbit/generated/android/Todo Orbit/gradlew.bat +93 -0
  31. package/examples/todo-orbit/generated/android/Todo Orbit/settings.gradle.kts +18 -0
  32. package/examples/todo-orbit/generated/ios/Todo Orbit/README.md +29 -0
  33. package/examples/todo-orbit/generated/ios/Todo Orbit/Resources/en.lproj/Localizable.strings +118 -0
  34. package/examples/todo-orbit/generated/ios/Todo Orbit/Resources/ru.lproj/Localizable.strings +118 -0
  35. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/App/TodoOrbitApp.swift +50 -0
  36. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Components/OrbitChrome.swift +204 -0
  37. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Components/SchedulePreviewView.swift +126 -0
  38. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Components/TrendChartView.swift +70 -0
  39. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Flows/RecurringRuleSheet.swift +123 -0
  40. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Flows/TaskEditorSheet.swift +60 -0
  41. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Models/DomainModels.swift +238 -0
  42. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Screens/AnalyticsView.swift +94 -0
  43. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Screens/SettingsView.swift +74 -0
  44. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Screens/TasksHomeView.swift +363 -0
  45. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Support/AppModel.swift +324 -0
  46. package/examples/todo-orbit/generated/ios/Todo Orbit/TodoOrbit.xcodeproj/project.pbxproj +408 -0
  47. package/examples/todo-orbit/generated/ios/Todo Orbit/TodoOrbit.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  48. package/examples/todo-orbit/generated/ios/Todo Orbit/TodoOrbit.xcodeproj/xcshareddata/xcschemes/TodoOrbit.xcscheme +79 -0
  49. package/examples/todo-orbit/generated/ios/Todo Orbit/project.yml +24 -0
  50. package/examples/todo-orbit/generated/web/Todo Orbit/index.html +16 -0
  51. package/examples/todo-orbit/generated/web/Todo Orbit/package-lock.json +1087 -0
  52. package/examples/todo-orbit/generated/web/Todo Orbit/package.json +24 -0
  53. package/examples/todo-orbit/generated/web/Todo Orbit/src/App.tsx +2114 -0
  54. package/examples/todo-orbit/generated/web/Todo Orbit/src/main.tsx +13 -0
  55. package/examples/todo-orbit/generated/web/Todo Orbit/src/styles.css +886 -0
  56. package/examples/todo-orbit/generated/web/Todo Orbit/tsconfig.json +19 -0
  57. package/examples/todo-orbit/generated/web/Todo Orbit/vite.config.ts +6 -0
  58. package/examples/todo-orbit/openuispec/README.md +158 -0
  59. package/examples/todo-orbit/openuispec/contracts/.gitkeep +0 -0
  60. package/examples/todo-orbit/openuispec/contracts/action_trigger.yaml +28 -0
  61. package/examples/todo-orbit/openuispec/contracts/collection.yaml +32 -0
  62. package/examples/todo-orbit/openuispec/contracts/data_display.yaml +38 -0
  63. package/examples/todo-orbit/openuispec/contracts/feedback.yaml +32 -0
  64. package/examples/todo-orbit/openuispec/contracts/input_field.yaml +52 -0
  65. package/examples/todo-orbit/openuispec/contracts/nav_container.yaml +47 -0
  66. package/examples/todo-orbit/openuispec/contracts/surface.yaml +28 -0
  67. package/examples/todo-orbit/openuispec/contracts/x_schedule_preview.yaml +134 -0
  68. package/examples/todo-orbit/openuispec/contracts/x_task_trend_chart.yaml +139 -0
  69. package/examples/todo-orbit/openuispec/flows/.gitkeep +0 -0
  70. package/examples/todo-orbit/openuispec/flows/create_recurring_rule.yaml +253 -0
  71. package/examples/todo-orbit/openuispec/flows/create_task.yaml +118 -0
  72. package/examples/todo-orbit/openuispec/flows/edit_task.yaml +126 -0
  73. package/examples/todo-orbit/openuispec/locales/.gitkeep +0 -0
  74. package/examples/todo-orbit/openuispec/locales/en.json +150 -0
  75. package/examples/todo-orbit/openuispec/locales/ru.json +150 -0
  76. package/examples/todo-orbit/openuispec/openuispec.yaml +122 -0
  77. package/examples/todo-orbit/openuispec/platform/.gitkeep +0 -0
  78. package/examples/todo-orbit/openuispec/platform/android.yaml +19 -0
  79. package/examples/todo-orbit/openuispec/platform/ios.yaml +20 -0
  80. package/examples/todo-orbit/openuispec/platform/web.yaml +22 -0
  81. package/examples/todo-orbit/openuispec/screens/.gitkeep +0 -0
  82. package/examples/todo-orbit/openuispec/screens/analytics.yaml +139 -0
  83. package/examples/todo-orbit/openuispec/screens/home.yaml +172 -0
  84. package/examples/todo-orbit/openuispec/screens/settings.yaml +148 -0
  85. package/examples/todo-orbit/openuispec/screens/task_detail.yaml +223 -0
  86. package/examples/todo-orbit/openuispec/tokens/.gitkeep +0 -0
  87. package/examples/todo-orbit/openuispec/tokens/color.yaml +93 -0
  88. package/examples/todo-orbit/openuispec/tokens/elevation.yaml +25 -0
  89. package/examples/todo-orbit/openuispec/tokens/icons.yaml +92 -0
  90. package/examples/todo-orbit/openuispec/tokens/layout.yaml +107 -0
  91. package/examples/todo-orbit/openuispec/tokens/motion.yaml +39 -0
  92. package/examples/todo-orbit/openuispec/tokens/spacing.yaml +18 -0
  93. package/examples/todo-orbit/openuispec/tokens/themes.yaml +23 -0
  94. package/examples/todo-orbit/openuispec/tokens/typography.yaml +52 -0
  95. package/package.json +1 -1
  96. package/schema/validate.ts +271 -4
  97. package/spec/openuispec-v0.1.md +80 -13
@@ -0,0 +1,92 @@
1
+ icons:
2
+ sizes:
3
+ sm:
4
+ semantic: "Small inline icon size"
5
+ value: 16
6
+ md:
7
+ semantic: "Default icon size for actions and list items"
8
+ value: 20
9
+ lg:
10
+ semantic: "Prominent icon size for empty states and hero elements"
11
+ value: 24
12
+
13
+ variants:
14
+ default: "outline"
15
+ suffixes:
16
+ _fill: "Filled variant for active or completed states"
17
+
18
+ fallback:
19
+ strategy: "name_passthrough"
20
+ missing_icon: "questionmark_circle"
21
+
22
+ registry:
23
+ actions:
24
+ plus:
25
+ semantic: "Create a new task or item"
26
+ platform:
27
+ ios: "plus"
28
+ android: "add"
29
+ web: "plus"
30
+ search:
31
+ semantic: "Search tasks"
32
+ platform:
33
+ ios: "magnifyingglass"
34
+ android: "search"
35
+ web: "search"
36
+ trash:
37
+ semantic: "Delete a task"
38
+ platform:
39
+ ios: "trash"
40
+ android: "delete"
41
+ web: "trash-2"
42
+ checkmark:
43
+ semantic: "Confirm completion"
44
+ variants: ["circle", "circle_fill", "list"]
45
+ platform:
46
+ ios: "checkmark"
47
+ android: "check"
48
+ web: "check"
49
+
50
+ objects:
51
+ calendar:
52
+ semantic: "Date and scheduling"
53
+ platform:
54
+ ios: "calendar"
55
+ android: "calendar_today"
56
+ web: "calendar"
57
+ gear:
58
+ semantic: "Settings and preferences"
59
+ variants: ["fill"]
60
+ platform:
61
+ ios: "gear"
62
+ android: "settings"
63
+ web: "settings"
64
+ globe:
65
+ semantic: "Language and locale selection"
66
+ platform:
67
+ ios: "globe"
68
+ android: "language"
69
+ web: "languages"
70
+ flag:
71
+ semantic: "Priority marker"
72
+ variants: ["fill"]
73
+ platform:
74
+ ios: "flag"
75
+ android: "flag"
76
+ web: "flag"
77
+
78
+ custom:
79
+ todo_orbit:
80
+ checkmark_list:
81
+ semantic: "Todo list navigation icon"
82
+ platform:
83
+ ios: "checklist"
84
+ android: "checklist"
85
+ web: "list-checks"
86
+ chart_line:
87
+ semantic: "Analytics and trend visualization icon"
88
+ variants: ["fill"]
89
+ platform:
90
+ ios: "chart.line.uptrend.xyaxis"
91
+ android: "insert_chart"
92
+ web: "chart-line"
@@ -0,0 +1,107 @@
1
+ layout:
2
+ size_classes:
3
+ compact:
4
+ semantic: "Phone-first single-column layouts"
5
+ width: { max: 640 }
6
+ columns: 4
7
+ margin: "spacing.md"
8
+ content_max_width: null
9
+ examples: "Phones in portrait, narrow browser windows"
10
+ regular:
11
+ semantic: "Tablet portrait and large phone landscape layouts"
12
+ width: { min: 641, max: 1024 }
13
+ columns: 8
14
+ margin: "spacing.lg"
15
+ content_max_width: 860
16
+ examples: "Tablets in portrait, wide phones in landscape"
17
+ expanded:
18
+ semantic: "Desktop and large-tablet multi-column layouts"
19
+ width: { min: 1025 }
20
+ columns: 12
21
+ margin: "spacing.xl"
22
+ content_max_width: 1240
23
+ examples: "Desktop browsers, tablets in landscape"
24
+
25
+ platform_mapping:
26
+ ios:
27
+ uses: "UIUserInterfaceSizeClass"
28
+ compact: ".compact horizontal"
29
+ regular: ".regular horizontal"
30
+ expanded: ".regular horizontal + width > 1024"
31
+ android:
32
+ uses: "WindowSizeClass"
33
+ compact: "WindowWidthSizeClass.Compact"
34
+ regular: "WindowWidthSizeClass.Medium"
35
+ expanded: "WindowWidthSizeClass.Expanded"
36
+ web:
37
+ uses: "media queries"
38
+ compact: "@media (max-width: 640px)"
39
+ regular: "@media (min-width: 641px) and (max-width: 1024px)"
40
+ expanded: "@media (min-width: 1025px)"
41
+
42
+ primitives:
43
+ stack:
44
+ semantic: "Vertical content flow"
45
+ props:
46
+ spacing: { type: token_ref, default: "spacing.md" }
47
+ align: { type: enum, values: [leading, center, trailing, stretch], default: stretch }
48
+ padding: { type: token_ref, required: false }
49
+ platform_mapping:
50
+ ios: "VStack"
51
+ android: "Column"
52
+ web: "flex-direction: column"
53
+ row:
54
+ semantic: "Horizontal content flow"
55
+ props:
56
+ spacing: { type: token_ref, default: "spacing.sm" }
57
+ align: { type: enum, values: [top, center, bottom, baseline, stretch], default: center }
58
+ justify: { type: enum, values: [start, center, end, space-between, space-around], default: start }
59
+ wrap: { type: bool, default: false }
60
+ platform_mapping:
61
+ ios: "HStack"
62
+ android: "Row"
63
+ web: "flex-direction: row"
64
+ grid:
65
+ semantic: "Responsive 2D arrangement"
66
+ props:
67
+ columns: { type: "int or adaptive_map", default: 2 }
68
+ gap: { type: token_ref, default: "spacing.md" }
69
+ platform_mapping:
70
+ ios: "LazyVGrid"
71
+ android: "LazyVerticalGrid"
72
+ web: "display: grid"
73
+ scroll_vertical:
74
+ semantic: "Scrollable vertical canvas"
75
+ props:
76
+ safe_area: { type: bool, default: true }
77
+ padding: { type: token_ref, required: false }
78
+ platform_mapping:
79
+ ios: "ScrollView(.vertical)"
80
+ android: "LazyColumn or verticalScroll"
81
+ web: "overflow-y: auto"
82
+ split_view:
83
+ semantic: "Master-detail two-pane layout"
84
+ props:
85
+ primary_width: { type: "fraction or token_ref", default: 0.36 }
86
+ divider: { type: bool, default: true }
87
+ collapse_at: { type: size_class, default: "compact" }
88
+ platform_mapping:
89
+ ios: "NavigationSplitView"
90
+ android: "ListDetailPaneScaffold"
91
+ web: "CSS Grid with two columns"
92
+
93
+ reflow_rules:
94
+ action_trigger:
95
+ compact: { full_width: true }
96
+ regular: { full_width: false }
97
+ nav_container:
98
+ compact: { variant: "tab_bar" }
99
+ regular: { variant: "rail" }
100
+ expanded: { variant: "sidebar" }
101
+ task_actions:
102
+ compact: { layout: stack }
103
+ regular: { layout: row }
104
+ stat_group:
105
+ compact: { layout: grid, columns: 2 }
106
+ regular: { layout: row }
107
+ expanded: { layout: row }
@@ -0,0 +1,39 @@
1
+ motion:
2
+ duration:
3
+ instant: 100
4
+ quick: 180
5
+ normal: 280
6
+ slow: 420
7
+
8
+ easing:
9
+ default: "ease-out"
10
+ enter: "ease-out"
11
+ exit: "ease-in"
12
+ emphasis: "cubic-bezier(0.2, 0, 0, 1)"
13
+
14
+ reduced_motion: "remove-animation"
15
+
16
+ patterns:
17
+ press_feedback:
18
+ duration: "instant"
19
+ property: "scale"
20
+ value: 0.98
21
+ state_change:
22
+ duration: "quick"
23
+ property: "opacity, border-color, background"
24
+ screen_enter:
25
+ duration: "normal"
26
+ easing: "enter"
27
+ pattern: "slide-from-trailing"
28
+ screen_exit:
29
+ duration: "quick"
30
+ easing: "exit"
31
+ pattern: "slide-to-leading"
32
+ checkbox_check:
33
+ duration: "quick"
34
+ easing: "emphasis"
35
+ pattern: "scale-bounce"
36
+ modal_present:
37
+ duration: "normal"
38
+ easing: "enter"
39
+ pattern: "fade-and-lift"
@@ -0,0 +1,18 @@
1
+ spacing:
2
+ base_unit: 4
3
+ platform_flex: 0.15
4
+
5
+ scale:
6
+ none: 0
7
+ xs: 4
8
+ sm: 8
9
+ md: { base: 16, range: [12, 16] }
10
+ lg: { base: 24, range: [20, 24] }
11
+ xl: 32
12
+ xxl: 40
13
+
14
+ aliases:
15
+ page_margin: { horizontal: md, vertical: md }
16
+ card_padding: { all: md }
17
+ section_gap: lg
18
+ inline_gap: sm
@@ -0,0 +1,23 @@
1
+ themes:
2
+ default: "light"
3
+
4
+ variants:
5
+ light:
6
+ surface.primary: { lightness: [97, 100] }
7
+ surface.secondary: { lightness: [93, 97] }
8
+ surface.tertiary: { lightness: [89, 94] }
9
+ text.primary: { lightness: [8, 14] }
10
+ text.secondary: { lightness: [36, 46] }
11
+ border.default: { opacity: 0.22 }
12
+ dark:
13
+ surface.primary: { lightness: [10, 14] }
14
+ surface.secondary: { lightness: [14, 18] }
15
+ surface.tertiary: { lightness: [18, 24] }
16
+ text.primary: { lightness: [90, 95] }
17
+ text.secondary: { lightness: [62, 70] }
18
+ border.default: { opacity: 0.18 }
19
+
20
+ platform:
21
+ ios: { supports_dynamic: true }
22
+ android: { dynamic_color: true }
23
+ web: { prefers_color_scheme: true, css_custom_properties: true }
@@ -0,0 +1,52 @@
1
+ typography:
2
+ font_family:
3
+ primary:
4
+ semantic: "Main UI typeface for the todo app"
5
+ value: "Manrope"
6
+ fallback_strategy: "modern-sans"
7
+ platform:
8
+ ios: { system_alternative: "SF Pro" }
9
+ android: { system_alternative: "Google Sans" }
10
+ web: { load_strategy: "swap", source: "google_fonts" }
11
+
12
+ scale:
13
+ display:
14
+ semantic: "Large empty-state and hero copy"
15
+ size: { base: 30, range: [28, 34] }
16
+ weight: 700
17
+ tracking: -0.02
18
+ line_height: 1.15
19
+ heading_lg:
20
+ semantic: "Primary screen titles"
21
+ size: { base: 24, range: [22, 28] }
22
+ weight: 700
23
+ tracking: -0.02
24
+ line_height: 1.2
25
+ heading:
26
+ semantic: "Section headings and modal titles"
27
+ size: { base: 18, range: [17, 20] }
28
+ weight: 600
29
+ tracking: -0.01
30
+ line_height: 1.3
31
+ heading_sm:
32
+ semantic: "Compact emphasized labels"
33
+ size: { base: 15, range: [14, 17] }
34
+ weight: 600
35
+ line_height: 1.35
36
+ body:
37
+ semantic: "Primary body copy"
38
+ size: { base: 16, range: [15, 17] }
39
+ weight: 400
40
+ line_height: 1.5
41
+ body_sm:
42
+ semantic: "Supporting text and subtitles"
43
+ size: { base: 14, range: [13, 15] }
44
+ weight: 400
45
+ tracking: 0.004
46
+ line_height: 1.45
47
+ caption:
48
+ semantic: "Tiny labels and metadata"
49
+ size: { base: 12, range: [11, 13] }
50
+ weight: 500
51
+ tracking: 0.015
52
+ line_height: 1.35
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openuispec",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "A semantic UI specification format for AI-native, platform-native app development",
@@ -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 {
@@ -146,8 +407,6 @@ function validateFile(
146
407
  return errors.length;
147
408
  }
148
409
 
149
- // ── validation groups ────────────────────────────────────────────────
150
-
151
410
  // ── includes resolution ──────────────────────────────────────────────
152
411
 
153
412
  interface Includes {
@@ -233,7 +492,11 @@ const GROUPS: Record<string, ValidationGroup> = {
233
492
  let errors = 0;
234
493
  const dir = resolveInclude(projectDir, includes.screens);
235
494
  for (const f of listFiles(dir, ".yaml")) {
236
- errors += validateFile(ajv, f, `${BASE}screen.schema.json`);
495
+ const schemaErrors = validateFile(ajv, f, `${BASE}screen.schema.json`);
496
+ errors += schemaErrors;
497
+ if (schemaErrors === 0) {
498
+ errors += lintScreenFile(f);
499
+ }
237
500
  }
238
501
  return errors;
239
502
  },
@@ -245,7 +508,11 @@ const GROUPS: Record<string, ValidationGroup> = {
245
508
  let errors = 0;
246
509
  const dir = resolveInclude(projectDir, includes.flows);
247
510
  for (const f of listFiles(dir, ".yaml")) {
248
- errors += validateFile(ajv, f, `${BASE}flow.schema.json`);
511
+ const schemaErrors = validateFile(ajv, f, `${BASE}flow.schema.json`);
512
+ errors += schemaErrors;
513
+ if (schemaErrors === 0) {
514
+ errors += lintFlowFile(f);
515
+ }
249
516
  }
250
517
  return errors;
251
518
  },