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.
- android_resource_resolver.py +370 -0
- android_xml_support.py +230 -0
- apple_resource_resolver.py +141 -0
- coverage_report.py +134 -0
- fidelity_adapter_api.py +89 -0
- fidelity_adapters.py +508 -0
- fidelity_core.py +241 -0
- fidelity_platform_adapters.py +902 -0
- ir_contracts.py +76 -0
- native_render_android.py +160 -0
- native_render_apple.py +165 -0
- native_render_contracts.py +148 -0
- native_render_registry.py +86 -0
- quality_common.py +217 -0
- render_preview.py +1797 -0
- scan_ui.py +1485 -0
- scoped_context.py +458 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/SKILL.md +52 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/adapter-api.md +20 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/agent-integrations.md +39 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/cache-protocol.md +61 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/design-modes.md +109 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/fidelity.md +53 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/ir-schema.md +412 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/native-rendering.md +40 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/platform-profiles.json +84 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/platform-standards.md +131 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/platforms.md +40 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/quality-automation.md +77 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/review-workflow.md +135 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/ui-guidance-mode.md +22 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/ui-policy-schema.md +83 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/ui-reviewer.md +175 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/validation.md +34 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/workbench-ui.md +108 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/schemas/native-render-state.schema.json +71 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/schemas/ui-agent-job.schema.json +36 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/schemas/ui-graph.schema.json +46 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/schemas/ui-ir.patch.schema.json +36 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/schemas/ui-ir.schema.json +296 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/schemas/uidw-config.schema.json +68 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/android_resource_resolver.py +370 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/android_xml_support.py +230 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/apple_resource_resolver.py +141 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/coverage_report.py +134 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/fidelity_adapter_api.py +89 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/fidelity_adapters.py +508 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/fidelity_core.py +241 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/fidelity_platform_adapters.py +902 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/generate_interaction_matrix.py +166 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/ir_contracts.py +76 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/merge_review_state.py +267 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/native_render_android.py +160 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/native_render_apple.py +165 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/native_render_contracts.py +148 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/native_render_registry.py +86 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/quality_common.py +217 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/render_preview.py +1797 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/scan_ui.py +1485 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/scoped_context.py +458 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/smoke_preview.js +978 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/uidw.py +2821 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/uidw_mcp.py +282 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/validate_platform_profiles.py +93 -0
- ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/visual_regression.py +141 -0
- ui_design_workbench_cli-0.5.0.dist-info/METADATA +133 -0
- ui_design_workbench_cli-0.5.0.dist-info/RECORD +73 -0
- ui_design_workbench_cli-0.5.0.dist-info/WHEEL +5 -0
- ui_design_workbench_cli-0.5.0.dist-info/entry_points.txt +3 -0
- ui_design_workbench_cli-0.5.0.dist-info/top_level.txt +20 -0
- uidw.py +2821 -0
- uidw_mcp.py +282 -0
- 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
|