ui-design-workbench-cli 0.5.0__py3-none-any.whl

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 (73) hide show
  1. android_resource_resolver.py +370 -0
  2. android_xml_support.py +230 -0
  3. apple_resource_resolver.py +141 -0
  4. coverage_report.py +134 -0
  5. fidelity_adapter_api.py +89 -0
  6. fidelity_adapters.py +508 -0
  7. fidelity_core.py +241 -0
  8. fidelity_platform_adapters.py +902 -0
  9. ir_contracts.py +76 -0
  10. native_render_android.py +160 -0
  11. native_render_apple.py +165 -0
  12. native_render_contracts.py +148 -0
  13. native_render_registry.py +86 -0
  14. quality_common.py +217 -0
  15. render_preview.py +1797 -0
  16. scan_ui.py +1485 -0
  17. scoped_context.py +458 -0
  18. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/SKILL.md +52 -0
  19. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/adapter-api.md +20 -0
  20. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/agent-integrations.md +39 -0
  21. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/cache-protocol.md +61 -0
  22. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/design-modes.md +109 -0
  23. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/fidelity.md +53 -0
  24. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/ir-schema.md +412 -0
  25. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/native-rendering.md +40 -0
  26. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/platform-profiles.json +84 -0
  27. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/platform-standards.md +131 -0
  28. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/platforms.md +40 -0
  29. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/quality-automation.md +77 -0
  30. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/review-workflow.md +135 -0
  31. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/ui-guidance-mode.md +22 -0
  32. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/ui-policy-schema.md +83 -0
  33. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/ui-reviewer.md +175 -0
  34. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/validation.md +34 -0
  35. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/workbench-ui.md +108 -0
  36. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/schemas/native-render-state.schema.json +71 -0
  37. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/schemas/ui-agent-job.schema.json +36 -0
  38. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/schemas/ui-graph.schema.json +46 -0
  39. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/schemas/ui-ir.patch.schema.json +36 -0
  40. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/schemas/ui-ir.schema.json +296 -0
  41. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/schemas/uidw-config.schema.json +68 -0
  42. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/android_resource_resolver.py +370 -0
  43. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/android_xml_support.py +230 -0
  44. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/apple_resource_resolver.py +141 -0
  45. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/coverage_report.py +134 -0
  46. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/fidelity_adapter_api.py +89 -0
  47. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/fidelity_adapters.py +508 -0
  48. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/fidelity_core.py +241 -0
  49. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/fidelity_platform_adapters.py +902 -0
  50. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/generate_interaction_matrix.py +166 -0
  51. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/ir_contracts.py +76 -0
  52. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/merge_review_state.py +267 -0
  53. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/native_render_android.py +160 -0
  54. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/native_render_apple.py +165 -0
  55. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/native_render_contracts.py +148 -0
  56. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/native_render_registry.py +86 -0
  57. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/quality_common.py +217 -0
  58. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/render_preview.py +1797 -0
  59. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/scan_ui.py +1485 -0
  60. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/scoped_context.py +458 -0
  61. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/smoke_preview.js +978 -0
  62. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/uidw.py +2821 -0
  63. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/uidw_mcp.py +282 -0
  64. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/validate_platform_profiles.py +93 -0
  65. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/visual_regression.py +141 -0
  66. ui_design_workbench_cli-0.5.0.dist-info/METADATA +133 -0
  67. ui_design_workbench_cli-0.5.0.dist-info/RECORD +73 -0
  68. ui_design_workbench_cli-0.5.0.dist-info/WHEEL +5 -0
  69. ui_design_workbench_cli-0.5.0.dist-info/entry_points.txt +3 -0
  70. ui_design_workbench_cli-0.5.0.dist-info/top_level.txt +20 -0
  71. uidw.py +2821 -0
  72. uidw_mcp.py +282 -0
  73. validate_platform_profiles.py +93 -0
quality_common.py ADDED
@@ -0,0 +1,217 @@
1
+ #!/usr/bin/env python3
2
+ """Shared deterministic helpers for UI Design Workbench quality gates."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ import os
8
+ import re
9
+ import sys
10
+ from pathlib import Path
11
+ from typing import Any, Iterable
12
+
13
+
14
+ def resolve_profile_catalog() -> Path:
15
+ candidates = []
16
+ if os.environ.get("UIDW_HOME"):
17
+ candidates.append(Path(os.environ["UIDW_HOME"]).expanduser() / "references" / "platform-profiles.json")
18
+ candidates.extend([
19
+ Path(__file__).resolve().parent.parent / "references" / "platform-profiles.json",
20
+ Path(__file__).resolve().parent / "share" / "ui-design-workbench" / "references" / "platform-profiles.json",
21
+ Path(sys.prefix) / "share" / "ui-design-workbench" / "references" / "platform-profiles.json",
22
+ ])
23
+ return next((item for item in candidates if item.is_file()), candidates[0])
24
+
25
+
26
+ PROFILE_CATALOG_PATH = resolve_profile_catalog()
27
+
28
+
29
+ def read_json(path: str | Path) -> dict[str, Any]:
30
+ value = json.loads(Path(path).read_text(encoding="utf-8"))
31
+ if not isinstance(value, dict):
32
+ raise ValueError(f"Expected a JSON object in {path}")
33
+ return value
34
+
35
+
36
+ def write_json(path: str | Path, value: Any) -> None:
37
+ target = Path(path)
38
+ target.parent.mkdir(parents=True, exist_ok=True)
39
+ target.write_text(json.dumps(value, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
40
+
41
+
42
+ def profile_catalog() -> dict[str, Any]:
43
+ return read_json(PROFILE_CATALOG_PATH)
44
+
45
+
46
+ def platform_family(value: Any) -> str | None:
47
+ platform = str(value or "").strip().lower()
48
+ if any(marker in platform for marker in ("android-tv", "compose-tv", "leanback", "react-native", "react_native", "mac-catalyst")):
49
+ return None
50
+ if platform in {"windows", "windows-winui", "windows-wpf", "windows-xaml", "winui", "winui3", "wpf", "windows-app-sdk"} or platform.startswith("windows-"):
51
+ return "windows"
52
+ if platform in {"macos", "swiftui-macos", "appkit"} or "macos" in platform:
53
+ return "macos"
54
+ if "android" in platform:
55
+ return "android"
56
+ if platform in {"ios", "swiftui", "uikit"} or "apple" in platform:
57
+ return "ios"
58
+ if platform == "flutter":
59
+ return "flutter"
60
+ if platform in {"web", "react-web", "vue", "svelte"}:
61
+ return "web"
62
+ return None
63
+
64
+
65
+ def target_families(ir: dict[str, Any]) -> set[str]:
66
+ explicit = ir.get("design", {}).get("targetPlatforms", [])
67
+ values = explicit or ir.get("platforms", [])
68
+ families = {platform_family(item) for item in values}
69
+ families.discard(None)
70
+ return families
71
+
72
+
73
+ def framework_adapters(ir: dict[str, Any]) -> set[str]:
74
+ known = profile_catalog().get("frameworkAdapters", {})
75
+ found: set[str] = set()
76
+ for value in ir.get("platforms", []):
77
+ normalized = str(value or "").strip().lower()
78
+ if normalized in known:
79
+ found.add(normalized)
80
+ elif "flutter" in normalized:
81
+ found.add("flutter")
82
+ return found
83
+
84
+
85
+ def walk_tree_screen_ids(items: Iterable[Any]) -> list[str]:
86
+ result: list[str] = []
87
+ for item in items or []:
88
+ if not isinstance(item, dict):
89
+ continue
90
+ screen_id = item.get("screenId") or item.get("screen")
91
+ if screen_id:
92
+ result.append(str(screen_id))
93
+ result.extend(walk_tree_screen_ids(item.get("children", [])))
94
+ return result
95
+
96
+
97
+ def walk_tree_screen_paths(items: Iterable[Any], path: tuple[str, ...] = ()) -> dict[str, list[str]]:
98
+ result: dict[str, list[str]] = {}
99
+ for item in items or []:
100
+ if not isinstance(item, dict):
101
+ continue
102
+ screen_id = item.get("screenId") or item.get("screen")
103
+ if screen_id:
104
+ result[str(screen_id)] = list(path)
105
+ continue
106
+ label = str(item.get("label") or item.get("name") or item.get("id") or "").strip()
107
+ result.update(walk_tree_screen_paths(item.get("children", []), (*path, label) if label else path))
108
+ return result
109
+
110
+
111
+ def hierarchy_issues(ir: dict[str, Any]) -> list[dict[str, Any]]:
112
+ """Compare authored screenTree paths with source-evidenced hierarchy metadata."""
113
+ paths = walk_tree_screen_paths(ir.get("screenTree", []))
114
+ screens = ir.get("screens", [])
115
+ discovered = [item for item in ir.get("discoveredScreens", []) if isinstance(item, dict)]
116
+ screen_by_evidence: dict[tuple[str, str], str] = {}
117
+ screen_by_fragment: dict[str, str] = {}
118
+ for screen in screens:
119
+ screen_id = str(screen.get("id") or "")
120
+ source = screen.get("source", {}) if isinstance(screen.get("source"), dict) else {}
121
+ screen_by_evidence[(str(source.get("file") or ""), str(source.get("symbol") or screen.get("name") or ""))] = screen_id
122
+ if screen.get("fragment"):
123
+ screen_by_fragment[str(screen["fragment"])] = screen_id
124
+
125
+ def normalized(values: Iterable[Any]) -> list[str]:
126
+ return [re.sub(r"[^\w]+", " ", str(value).casefold()).strip() for value in values if str(value).strip()]
127
+
128
+ def contains_path(actual: list[str], expected: list[str]) -> bool:
129
+ if not expected:
130
+ return True
131
+ return any(actual[index:index + len(expected)] == expected for index in range(len(actual) - len(expected) + 1))
132
+
133
+ issues: list[dict[str, Any]] = []
134
+ evidence_by_fragment = {str(item.get("fragment")): item for item in discovered if item.get("fragment")}
135
+ for candidate in discovered:
136
+ key = (str(candidate.get("file") or ""), str(candidate.get("name") or ""))
137
+ screen_id = screen_by_evidence.get(key) or screen_by_fragment.get(str(candidate.get("fragment") or ""))
138
+ if not screen_id or screen_id not in paths:
139
+ continue
140
+ expected = normalized(candidate.get("groupPath", []))
141
+ actual = normalized(paths[screen_id])
142
+ if expected and not contains_path(actual, expected):
143
+ issues.append({
144
+ "code": "group-path-mismatch",
145
+ "screenId": screen_id,
146
+ "expected": candidate.get("groupPath", []),
147
+ "actual": paths[screen_id],
148
+ })
149
+ parent_fragment = str(candidate.get("parentFragment") or "")
150
+ if not parent_fragment:
151
+ continue
152
+ parent_candidate = evidence_by_fragment.get(parent_fragment, {})
153
+ parent_key = (str(parent_candidate.get("file") or ""), str(parent_candidate.get("name") or ""))
154
+ parent_id = screen_by_evidence.get(parent_key) or screen_by_fragment.get(parent_fragment)
155
+ if not parent_id or parent_id not in paths:
156
+ continue
157
+ parent_path = normalized(paths[parent_id])
158
+ child_path = normalized(paths[screen_id])
159
+ if len(child_path) <= len(parent_path) or child_path[:len(parent_path)] != parent_path:
160
+ issues.append({
161
+ "code": "parent-hierarchy-flattened",
162
+ "screenId": screen_id,
163
+ "parentScreenId": parent_id,
164
+ "parentPath": paths[parent_id],
165
+ "actual": paths[screen_id],
166
+ })
167
+ return issues
168
+
169
+
170
+ def navigation_graph_issues(ir: dict[str, Any]) -> list[dict[str, Any]]:
171
+ discovered = [item for item in ir.get("discoveredScreens", []) if isinstance(item, dict)]
172
+ nested = [item for item in discovered if item.get("parentFragment")]
173
+ graph = ir.get("navigationGraph")
174
+ if not nested and graph is None:
175
+ return []
176
+ if not isinstance(graph, dict):
177
+ return [{"code": "navigation-graph-missing"}]
178
+ screen_ids = {str(item.get("id")) for item in ir.get("screens", []) if item.get("id")}
179
+ graph_ids = {str(item.get("screenId")) for item in graph.get("nodes", []) if isinstance(item, dict) and item.get("screenId")}
180
+ issues: list[dict[str, Any]] = []
181
+ if graph_ids != screen_ids:
182
+ issues.append({"code": "navigation-graph-node-mismatch", "missing": sorted(screen_ids - graph_ids), "unknown": sorted(graph_ids - screen_ids)})
183
+ edges = {
184
+ (str(item.get("from")), str(item.get("to")), str(item.get("kind")))
185
+ for item in graph.get("edges", []) if isinstance(item, dict)
186
+ }
187
+ for source, target, kind in edges:
188
+ if source not in screen_ids or target not in screen_ids:
189
+ issues.append({"code": "navigation-graph-edge-invalid", "from": source, "to": target, "kind": kind})
190
+ screen_by_fragment = {str(item.get("fragment")): str(item.get("id")) for item in ir.get("screens", []) if item.get("fragment")}
191
+ candidate_screen = {
192
+ str(item.get("fragment")): screen_by_fragment.get(str(item.get("fragment")))
193
+ for item in discovered if item.get("fragment")
194
+ }
195
+ for item in nested:
196
+ child_id = candidate_screen.get(str(item.get("fragment")))
197
+ parent_id = candidate_screen.get(str(item.get("parentFragment")))
198
+ if child_id and parent_id and (parent_id, child_id, "open-logical-view") not in edges:
199
+ issues.append({"code": "navigation-parent-edge-missing", "from": parent_id, "to": child_id})
200
+ return issues
201
+
202
+
203
+ def node_screen_map(ir: dict[str, Any]) -> dict[str, set[str]]:
204
+ nodes = ir.get("nodes", {})
205
+ result: dict[str, set[str]] = {str(node_id): set() for node_id in nodes}
206
+
207
+ def visit(node_id: str, screen_id: str, seen: set[str]) -> None:
208
+ if node_id in seen or node_id not in nodes:
209
+ return
210
+ seen.add(node_id)
211
+ result.setdefault(node_id, set()).add(screen_id)
212
+ for child in nodes[node_id].get("children", []):
213
+ visit(str(child), screen_id, seen)
214
+
215
+ for screen in ir.get("screens", []):
216
+ visit(str(screen.get("root", "")), str(screen.get("id", "")), set())
217
+ return result