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
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Optional local stdio MCP facade over the deterministic UIDW CLI core."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import uidw
|
|
12
|
+
from native_render_registry import native_render_status
|
|
13
|
+
from scoped_context import apply_patch_file, build_scoped_context
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
SERVER_INSTRUCTIONS = (
|
|
17
|
+
"Start with ui_project, then call ui_scope for one screen or a small finding set. "
|
|
18
|
+
"If ui_project returns setupRequired, ask the user only for low, medium, or high, then call "
|
|
19
|
+
"ui_configure with that choice before continuing. "
|
|
20
|
+
"Do not request or embed the complete UI IR when bounded context is sufficient. "
|
|
21
|
+
"Use ui_prepare_job for review, proposal, or implementation handoffs. Review and proposal work "
|
|
22
|
+
"must preserve immutable baseline UI and return sparse ui-ir.patch.json operations; application "
|
|
23
|
+
"source changes require an explicit implementation request. Use ui_build_preview for projection "
|
|
24
|
+
"checks only; do not infer UI/UX findings unless the user explicitly requests a review. "
|
|
25
|
+
"For Android or Apple visual accuracy, call ui_native_status and never describe source projection as a native render. "
|
|
26
|
+
"Native builds, simulators, and captures are explicit-only operations."
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _root(value: str | None, default: Path) -> Path:
|
|
31
|
+
path = Path(value).expanduser().resolve() if value else default.resolve()
|
|
32
|
+
if not path.is_dir():
|
|
33
|
+
raise ValueError(f"Repository directory does not exist: {path}")
|
|
34
|
+
return path
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _ir(root: Path, value: str | None) -> tuple[Path, dict[str, Any]]:
|
|
38
|
+
return uidw.load_ir_argument(uidw.state_paths(root), Path(value).expanduser() if value else None)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def project_summary(repo: str | None, default_repo: Path) -> dict[str, Any]:
|
|
42
|
+
root = _root(repo, default_repo)
|
|
43
|
+
initialization, paths, config = uidw.ensure_initialized(root)
|
|
44
|
+
ir = uidw.load_project_ir(paths) or {}
|
|
45
|
+
configuration = uidw.configuration_context(config)
|
|
46
|
+
setup_required = bool(configuration.get("setupRequired"))
|
|
47
|
+
return {
|
|
48
|
+
"status": initialization.get("status"),
|
|
49
|
+
"cache": initialization.get("initialization", {}).get("status"),
|
|
50
|
+
"repoRoot": str(root),
|
|
51
|
+
"uiIrFile": str(paths["ir"]),
|
|
52
|
+
"contextFile": str(paths["context"]),
|
|
53
|
+
"screens": [{"id": item.get("id"), "name": item.get("name"), "platform": item.get("platform")} for item in ir.get("screens", [])],
|
|
54
|
+
"findingCount": len(ir.get("review", {}).get("audit", {}).get("findings", [])),
|
|
55
|
+
"setupRequired": setup_required,
|
|
56
|
+
"detailLevel": configuration.get("detailLevel"),
|
|
57
|
+
"detailChoices": ["low", "medium", "high"] if setup_required else [],
|
|
58
|
+
"questionsForUser": configuration.get("questionsForUser", []),
|
|
59
|
+
"uiModeEnabled": bool(config.get(uidw.UI_MODE_KEY, {}).get("enabled")),
|
|
60
|
+
"next": (
|
|
61
|
+
"Ask the user to choose low, medium, or high, then call ui_configure."
|
|
62
|
+
if setup_required
|
|
63
|
+
else "Use ui_scope for one screen or a small finding set; do not read the full IR."
|
|
64
|
+
),
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def scoped_payload(
|
|
69
|
+
repo: str | None,
|
|
70
|
+
default_repo: Path,
|
|
71
|
+
ui_ir_file: str | None,
|
|
72
|
+
screen_ids: list[str],
|
|
73
|
+
finding_ids: list[str],
|
|
74
|
+
max_tokens: int,
|
|
75
|
+
) -> dict[str, Any]:
|
|
76
|
+
root = _root(repo, default_repo)
|
|
77
|
+
ir_path, ir = _ir(root, ui_ir_file)
|
|
78
|
+
return build_scoped_context(
|
|
79
|
+
ir,
|
|
80
|
+
screen_ids=screen_ids,
|
|
81
|
+
finding_ids=finding_ids,
|
|
82
|
+
token_budget=max_tokens,
|
|
83
|
+
ui_ir_file=str(ir_path),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def compact_workbench_result(result: dict[str, Any]) -> dict[str, Any]:
|
|
88
|
+
return {
|
|
89
|
+
key: result.get(key)
|
|
90
|
+
for key in ("version", "status", "previewFile", "url", "workflow", "initialization")
|
|
91
|
+
if result.get(key) is not None
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def configure_detail(repo: str | None, default_repo: Path, detail: str) -> dict[str, Any]:
|
|
96
|
+
root = _root(repo, default_repo)
|
|
97
|
+
normalized = detail.strip().lower()
|
|
98
|
+
if normalized not in {"low", "medium", "high"}:
|
|
99
|
+
return {
|
|
100
|
+
"status": "blocked",
|
|
101
|
+
"error": "detail must be low, medium, or high",
|
|
102
|
+
"detailChoices": ["low", "medium", "high"],
|
|
103
|
+
}
|
|
104
|
+
result = uidw.configure_project(root, "set", "detail", normalized)
|
|
105
|
+
configuration = result.get("configuration", {})
|
|
106
|
+
return {
|
|
107
|
+
"status": result.get("status"),
|
|
108
|
+
"repoRoot": result.get("repoRoot"),
|
|
109
|
+
"setupRequired": bool(configuration.get("setupRequired")),
|
|
110
|
+
"detailLevel": configuration.get("detailLevel"),
|
|
111
|
+
"mockData": configuration.get(uidw.MOCK_DATA_KEY),
|
|
112
|
+
"contextFile": result.get("contextFile"),
|
|
113
|
+
"next": "Use ui_scope, then ui_build_preview when the requested screens are ready.",
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def build_preview(
|
|
118
|
+
repo: str | None,
|
|
119
|
+
default_repo: Path,
|
|
120
|
+
ui_ir_file: str | None,
|
|
121
|
+
output_dir: str | None,
|
|
122
|
+
level: str,
|
|
123
|
+
) -> dict[str, Any]:
|
|
124
|
+
root = _root(repo, default_repo)
|
|
125
|
+
paths = uidw.state_paths(root)
|
|
126
|
+
configuration = uidw.configuration_context(uidw.load_config(paths["config"]))
|
|
127
|
+
if configuration.get("setupRequired"):
|
|
128
|
+
return {
|
|
129
|
+
"status": "needs-setup",
|
|
130
|
+
"setupRequired": True,
|
|
131
|
+
"detailChoices": ["low", "medium", "high"],
|
|
132
|
+
"questionsForUser": configuration.get("questionsForUser", []),
|
|
133
|
+
"next": "Ask the user to choose low, medium, or high, then call ui_configure.",
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
ir_path, _ = _ir(root, ui_ir_file)
|
|
137
|
+
destination = Path(output_dir).expanduser().resolve() if output_dir else ir_path.parent / "workbench"
|
|
138
|
+
cached_ir = paths["ir"].resolve()
|
|
139
|
+
allow_draft = ir_path.resolve() == cached_ir
|
|
140
|
+
try:
|
|
141
|
+
result = uidw.build_workbench(
|
|
142
|
+
root,
|
|
143
|
+
ir_path,
|
|
144
|
+
destination,
|
|
145
|
+
level,
|
|
146
|
+
allow_draft,
|
|
147
|
+
"generic",
|
|
148
|
+
False,
|
|
149
|
+
None,
|
|
150
|
+
None,
|
|
151
|
+
None,
|
|
152
|
+
None,
|
|
153
|
+
None,
|
|
154
|
+
)
|
|
155
|
+
except (RuntimeError, ValueError, OSError) as exc:
|
|
156
|
+
return {
|
|
157
|
+
"status": "blocked",
|
|
158
|
+
"error": str(exc),
|
|
159
|
+
"uiIrFile": str(ir_path),
|
|
160
|
+
"next": "Resolve the reported IR or fidelity problem, then call ui_build_preview again.",
|
|
161
|
+
}
|
|
162
|
+
return compact_workbench_result(result)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def create_server(default_repo: Path, name: str = "UI Design Workbench") -> Any:
|
|
166
|
+
try:
|
|
167
|
+
from mcp.server import MCPServer
|
|
168
|
+
except ImportError as exc:
|
|
169
|
+
raise RuntimeError(
|
|
170
|
+
"MCP support is optional. Install it with `pip install \"ui-design-workbench-cli[mcp]\"` "
|
|
171
|
+
"or `pip install \"mcp>=2,<3\"`. The regular uidw CLI does not require it."
|
|
172
|
+
) from exc
|
|
173
|
+
|
|
174
|
+
server = MCPServer(
|
|
175
|
+
name,
|
|
176
|
+
version=uidw.CLI_VERSION,
|
|
177
|
+
instructions=SERVER_INSTRUCTIONS,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
@server.tool()
|
|
181
|
+
def ui_project(repo: str = "") -> dict[str, Any]:
|
|
182
|
+
"""Get a compact cached UI catalog and the next bounded action."""
|
|
183
|
+
return project_summary(repo or None, default_repo)
|
|
184
|
+
|
|
185
|
+
@server.tool()
|
|
186
|
+
def ui_configure(detail: str, repo: str = "") -> dict[str, Any]:
|
|
187
|
+
"""Set the required low, medium, or high preview detail after the user chooses it."""
|
|
188
|
+
return configure_detail(repo or None, default_repo, detail)
|
|
189
|
+
|
|
190
|
+
@server.tool()
|
|
191
|
+
def ui_scope(
|
|
192
|
+
screen_ids: list[str],
|
|
193
|
+
finding_ids: list[str],
|
|
194
|
+
repo: str = "",
|
|
195
|
+
ui_ir_file: str = "",
|
|
196
|
+
max_tokens: int = 4000,
|
|
197
|
+
) -> dict[str, Any]:
|
|
198
|
+
"""Read complete UI evidence only for named screens/findings."""
|
|
199
|
+
return scoped_payload(repo or None, default_repo, ui_ir_file or None, screen_ids, finding_ids, max_tokens)
|
|
200
|
+
|
|
201
|
+
@server.tool()
|
|
202
|
+
def ui_prepare_job(
|
|
203
|
+
kind: str,
|
|
204
|
+
finding_ids: list[str],
|
|
205
|
+
repo: str = "",
|
|
206
|
+
ui_ir_file: str = "",
|
|
207
|
+
output_file: str = "",
|
|
208
|
+
scope: str = "current",
|
|
209
|
+
direct: bool = False,
|
|
210
|
+
) -> dict[str, Any]:
|
|
211
|
+
"""Prepare a portable bounded job plus context and ui-ir.patch.json."""
|
|
212
|
+
root = _root(repo or None, default_repo)
|
|
213
|
+
ir_path, ir = _ir(root, ui_ir_file or None)
|
|
214
|
+
if kind not in {"expert", "proposal", "implementation"}:
|
|
215
|
+
raise ValueError("kind must be expert, proposal, or implementation")
|
|
216
|
+
output = Path(output_file).expanduser().resolve() if output_file else ir_path.parent / f"ui-agent-job-{kind}.json"
|
|
217
|
+
return uidw.prepare_agent_job(root, ir_path, ir, kind, output, identifiers=finding_ids, scope=scope, direct=direct)
|
|
218
|
+
|
|
219
|
+
@server.tool()
|
|
220
|
+
def ui_apply_patch(
|
|
221
|
+
patch_file: str,
|
|
222
|
+
repo: str = "",
|
|
223
|
+
ui_ir_file: str = "",
|
|
224
|
+
output_file: str = "",
|
|
225
|
+
) -> dict[str, Any]:
|
|
226
|
+
"""Validate and apply sparse review/proposal operations without touching baseline UI."""
|
|
227
|
+
root = _root(repo or None, default_repo)
|
|
228
|
+
ir_path, _ = _ir(root, ui_ir_file or None)
|
|
229
|
+
output = Path(output_file).expanduser().resolve() if output_file else ir_path.with_name("ui-ir.patched.json")
|
|
230
|
+
return apply_patch_file(ir_path, Path(patch_file).expanduser(), output)
|
|
231
|
+
|
|
232
|
+
@server.tool()
|
|
233
|
+
def ui_build_preview(
|
|
234
|
+
repo: str = "",
|
|
235
|
+
ui_ir_file: str = "",
|
|
236
|
+
output_dir: str = "",
|
|
237
|
+
level: str = "quick",
|
|
238
|
+
) -> dict[str, Any]:
|
|
239
|
+
"""Render and projection-check the HTML workbench; never auto-review UX."""
|
|
240
|
+
return build_preview(repo or None, default_repo, ui_ir_file or None, output_dir or None, level)
|
|
241
|
+
|
|
242
|
+
@server.tool()
|
|
243
|
+
def ui_native_status(repo: str = "", platform: str = "all") -> dict[str, Any]:
|
|
244
|
+
"""Discover native Android/Apple render providers without executing builds or simulators."""
|
|
245
|
+
if platform not in {"all", "android", "apple"}:
|
|
246
|
+
raise ValueError("platform must be all, android, or apple")
|
|
247
|
+
root = _root(repo or None, default_repo)
|
|
248
|
+
return native_render_status(root, uidw.state_paths(root)["native"], platform)
|
|
249
|
+
|
|
250
|
+
@server.tool()
|
|
251
|
+
def ui_fidelity(identifier: str, repo: str = "", ui_ir_file: str = "") -> dict[str, Any]:
|
|
252
|
+
"""Explain one node/evidence id without returning the complete IR."""
|
|
253
|
+
root = _root(repo or None, default_repo)
|
|
254
|
+
_, ir = _ir(root, ui_ir_file or None)
|
|
255
|
+
return uidw.fidelity_command(ir, "explain", identifier, None, "json")
|
|
256
|
+
|
|
257
|
+
return server
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def run_server(default_repo: Path, name: str = "UI Design Workbench") -> None:
|
|
261
|
+
create_server(default_repo, name).run()
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|
265
|
+
parser = argparse.ArgumentParser(description="Local stdio MCP server for UI Design Workbench")
|
|
266
|
+
parser.add_argument("--repo", type=Path, default=Path.cwd())
|
|
267
|
+
parser.add_argument("--name", default="UI Design Workbench")
|
|
268
|
+
return parser.parse_args(argv)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def main() -> int:
|
|
272
|
+
args = parse_args()
|
|
273
|
+
try:
|
|
274
|
+
run_server(args.repo.resolve(), args.name)
|
|
275
|
+
except (RuntimeError, ValueError, OSError) as exc:
|
|
276
|
+
print(f"uidw mcp: {exc}", file=sys.stderr)
|
|
277
|
+
return 3
|
|
278
|
+
return 0
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
if __name__ == "__main__":
|
|
282
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Validate platform-profile selection and node standard references in UI IR."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from quality_common import framework_adapters, node_screen_map, platform_family, profile_catalog, read_json, target_families, write_json
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def validate_profiles(ir: dict[str, Any]) -> dict[str, Any]:
|
|
15
|
+
catalog = profile_catalog()
|
|
16
|
+
profiles = catalog.get("profiles", {})
|
|
17
|
+
adapters = catalog.get("frameworkAdapters", {})
|
|
18
|
+
families = target_families(ir)
|
|
19
|
+
selected = ir.get("design", {}).get("standardProfiles", {})
|
|
20
|
+
issues: list[dict[str, Any]] = []
|
|
21
|
+
resolved: dict[str, Any] = {}
|
|
22
|
+
|
|
23
|
+
if not families:
|
|
24
|
+
supported = ", ".join(sorted({str(item.get("family")) for item in profiles.values() if isinstance(item, dict) and item.get("family")}))
|
|
25
|
+
issues.append({"code": "target-platform-missing", "severity": "error", "message": f"design.targetPlatforms must resolve to a supported family: {supported}."})
|
|
26
|
+
|
|
27
|
+
for adapter_id in sorted(framework_adapters(ir)):
|
|
28
|
+
adapter = adapters[adapter_id]
|
|
29
|
+
if adapter.get("requiresExplicitTarget") and not ir.get("design", {}).get("targetPlatforms"):
|
|
30
|
+
issues.append({"code": "framework-target-ambiguous", "severity": "error", "adapter": adapter_id, "message": f"{adapter_id} requires explicit design.targetPlatforms."})
|
|
31
|
+
|
|
32
|
+
for family in sorted(families):
|
|
33
|
+
selection = selected.get(family)
|
|
34
|
+
if not isinstance(selection, dict):
|
|
35
|
+
issues.append({"code": "profile-missing", "severity": "error", "family": family, "message": f"Missing design.standardProfiles.{family}."})
|
|
36
|
+
continue
|
|
37
|
+
profile_id = selection.get("id")
|
|
38
|
+
if profile_id == "project":
|
|
39
|
+
if not selection.get("reason"):
|
|
40
|
+
issues.append({"code": "project-profile-unreasoned", "severity": "error", "family": family, "message": "A project profile needs a reason and evidence."})
|
|
41
|
+
else:
|
|
42
|
+
resolved[family] = {**selection, "family": family, "standardRefPrefixes": ["project."]}
|
|
43
|
+
continue
|
|
44
|
+
profile = profiles.get(profile_id)
|
|
45
|
+
if not profile or profile.get("family") != family:
|
|
46
|
+
issues.append({"code": "profile-invalid", "severity": "error", "family": family, "profile": profile_id, "message": f"Profile {profile_id!r} does not belong to {family}."})
|
|
47
|
+
continue
|
|
48
|
+
resolved[family] = {**profile, "id": profile_id, "source": selection.get("source")}
|
|
49
|
+
|
|
50
|
+
decisions = {item.get("id") for item in ir.get("design", {}).get("decisions", []) if isinstance(item, dict)}
|
|
51
|
+
screen_families = {str(screen.get("id")): platform_family(screen.get("platform")) for screen in ir.get("screens", [])}
|
|
52
|
+
node_screens = node_screen_map(ir)
|
|
53
|
+
for node_id, node in ir.get("nodes", {}).items():
|
|
54
|
+
if node.get("type") == "spacer" or node.get("decisionId") in decisions:
|
|
55
|
+
continue
|
|
56
|
+
shared = str(node.get("standardRef", ""))
|
|
57
|
+
per_platform = node.get("standardRefs", {}) if isinstance(node.get("standardRefs"), dict) else {}
|
|
58
|
+
node_families = {screen_families.get(screen_id) for screen_id in node_screens.get(str(node_id), set())}
|
|
59
|
+
node_families.discard(None)
|
|
60
|
+
node_families = node_families or families
|
|
61
|
+
for family in sorted(node_families):
|
|
62
|
+
profile = resolved.get(family)
|
|
63
|
+
if not profile:
|
|
64
|
+
continue
|
|
65
|
+
ref = str(per_platform.get(family, shared))
|
|
66
|
+
prefixes = tuple(profile.get("standardRefPrefixes", []))
|
|
67
|
+
if not ref.startswith(prefixes):
|
|
68
|
+
issues.append({"code": "standard-ref-mismatch", "severity": "error", "family": family, "node": node_id, "ref": ref or None, "allowedPrefixes": list(prefixes), "message": f"Node {node_id} lacks a {family} standard reference or decision."})
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
"version": 1,
|
|
72
|
+
"status": "blocked" if any(item["severity"] == "error" for item in issues) else "pass",
|
|
73
|
+
"targetFamilies": sorted(families),
|
|
74
|
+
"frameworkAdapters": sorted(framework_adapters(ir)),
|
|
75
|
+
"resolvedProfiles": resolved,
|
|
76
|
+
"issues": issues,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def main() -> int:
|
|
81
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
82
|
+
parser.add_argument("ir", help="Path to ui-ir.json")
|
|
83
|
+
parser.add_argument("--output", required=True, help="Path for platform-profile-report.json")
|
|
84
|
+
parser.add_argument("--strict", action="store_true", help="Exit non-zero when the report is blocked")
|
|
85
|
+
args = parser.parse_args()
|
|
86
|
+
report = validate_profiles(read_json(args.ir))
|
|
87
|
+
write_json(args.output, report)
|
|
88
|
+
print(Path(args.output).resolve())
|
|
89
|
+
return 2 if args.strict and report["status"] != "pass" else 0
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
if __name__ == "__main__":
|
|
93
|
+
sys.exit(main())
|
ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/visual_regression.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Compare preview screenshots and optional geometry snapshots with explicit tolerances."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import math
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
from PIL import Image
|
|
14
|
+
except ImportError: # pragma: no cover - depends on the selected workspace runtime
|
|
15
|
+
Image = None
|
|
16
|
+
|
|
17
|
+
from quality_common import read_json, write_json
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def compare_images(baseline_path: str, candidate_path: str, channel_tolerance: int, diff_path: str | None) -> dict[str, Any]:
|
|
21
|
+
baseline = Image.open(baseline_path).convert("RGBA")
|
|
22
|
+
candidate = Image.open(candidate_path).convert("RGBA")
|
|
23
|
+
if baseline.size != candidate.size:
|
|
24
|
+
return {"status": "fail", "baselineSize": list(baseline.size), "candidateSize": list(candidate.size), "reason": "image-size-mismatch"}
|
|
25
|
+
|
|
26
|
+
total = baseline.width * baseline.height
|
|
27
|
+
changed = 0
|
|
28
|
+
absolute_sum = 0
|
|
29
|
+
maximum = 0
|
|
30
|
+
bounds = [baseline.width, baseline.height, -1, -1]
|
|
31
|
+
overlay = Image.new("RGBA", baseline.size, (0, 0, 0, 0)) if diff_path else None
|
|
32
|
+
overlay_pixels = overlay.load() if overlay else None
|
|
33
|
+
|
|
34
|
+
baseline_data = baseline.get_flattened_data() if hasattr(baseline, "get_flattened_data") else baseline.getdata()
|
|
35
|
+
candidate_data = candidate.get_flattened_data() if hasattr(candidate, "get_flattened_data") else candidate.getdata()
|
|
36
|
+
for index, (before, after) in enumerate(zip(baseline_data, candidate_data)):
|
|
37
|
+
differences = tuple(abs(int(before[channel]) - int(after[channel])) for channel in range(4))
|
|
38
|
+
pixel_max = max(differences)
|
|
39
|
+
absolute_sum += sum(differences)
|
|
40
|
+
maximum = max(maximum, pixel_max)
|
|
41
|
+
if pixel_max <= channel_tolerance:
|
|
42
|
+
continue
|
|
43
|
+
changed += 1
|
|
44
|
+
x = index % baseline.width
|
|
45
|
+
y = index // baseline.width
|
|
46
|
+
bounds[0] = min(bounds[0], x)
|
|
47
|
+
bounds[1] = min(bounds[1], y)
|
|
48
|
+
bounds[2] = max(bounds[2], x)
|
|
49
|
+
bounds[3] = max(bounds[3], y)
|
|
50
|
+
if overlay_pixels is not None:
|
|
51
|
+
overlay_pixels[x, y] = (239, 68, 68, 220)
|
|
52
|
+
|
|
53
|
+
if overlay and diff_path:
|
|
54
|
+
target = Path(diff_path)
|
|
55
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
56
|
+
highlighted = candidate.copy()
|
|
57
|
+
highlighted.alpha_composite(overlay)
|
|
58
|
+
highlighted.save(target)
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
"status": "pass",
|
|
62
|
+
"baselineSize": list(baseline.size),
|
|
63
|
+
"candidateSize": list(candidate.size),
|
|
64
|
+
"channelTolerance": channel_tolerance,
|
|
65
|
+
"changedPixels": changed,
|
|
66
|
+
"changedRatio": round(changed / total, 8) if total else 0,
|
|
67
|
+
"meanAbsoluteError": round(absolute_sum / (total * 4), 5) if total else 0,
|
|
68
|
+
"maximumChannelError": maximum,
|
|
69
|
+
"changedBounds": None if changed == 0 else bounds,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def compare_geometry(baseline_path: str, candidate_path: str, tolerance: float) -> dict[str, Any]:
|
|
74
|
+
baseline = read_json(baseline_path)
|
|
75
|
+
candidate = read_json(candidate_path)
|
|
76
|
+
before = baseline.get("elements", {})
|
|
77
|
+
after = candidate.get("elements", {})
|
|
78
|
+
missing = sorted(set(before) - set(after))
|
|
79
|
+
unexpected = sorted(set(after) - set(before))
|
|
80
|
+
changes: list[dict[str, Any]] = []
|
|
81
|
+
for key in sorted(set(before) & set(after)):
|
|
82
|
+
first, second = before[key], after[key]
|
|
83
|
+
if first is None or second is None:
|
|
84
|
+
if first != second:
|
|
85
|
+
changes.append({"element": key, "field": "presence", "before": first, "after": second})
|
|
86
|
+
continue
|
|
87
|
+
for field in ("x", "y", "width", "height"):
|
|
88
|
+
a, b = first.get(field), second.get(field)
|
|
89
|
+
if isinstance(a, (int, float)) and isinstance(b, (int, float)) and math.fabs(a - b) > tolerance:
|
|
90
|
+
changes.append({"element": key, "field": field, "before": a, "after": b, "delta": round(b - a, 3)})
|
|
91
|
+
for field in ("visible", "overflowX", "overflowY"):
|
|
92
|
+
if first.get(field) != second.get(field):
|
|
93
|
+
changes.append({"element": key, "field": field, "before": first.get(field), "after": second.get(field)})
|
|
94
|
+
overlaps = candidate.get("overlaps", [])
|
|
95
|
+
return {
|
|
96
|
+
"status": "fail" if missing or unexpected or changes or overlaps else "pass",
|
|
97
|
+
"toleranceCssPx": tolerance,
|
|
98
|
+
"missingElements": missing,
|
|
99
|
+
"unexpectedElements": unexpected,
|
|
100
|
+
"changes": changes,
|
|
101
|
+
"candidateOverlaps": overlaps,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def main() -> int:
|
|
106
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
107
|
+
parser.add_argument("--baseline", required=True, help="Approved PNG screenshot")
|
|
108
|
+
parser.add_argument("--candidate", required=True, help="Candidate PNG screenshot")
|
|
109
|
+
parser.add_argument("--output", required=True, help="Path for visual-regression.json")
|
|
110
|
+
parser.add_argument("--diff-image", help="Optional highlighted PNG output")
|
|
111
|
+
parser.add_argument("--baseline-geometry", help="Approved geometry JSON from smoke_preview.js")
|
|
112
|
+
parser.add_argument("--candidate-geometry", help="Candidate geometry JSON from smoke_preview.js")
|
|
113
|
+
parser.add_argument("--channel-tolerance", type=int, default=12)
|
|
114
|
+
parser.add_argument("--max-changed-ratio", type=float, default=.005)
|
|
115
|
+
parser.add_argument("--max-mean-error", type=float, default=1.0)
|
|
116
|
+
parser.add_argument("--geometry-tolerance", type=float, default=1.0)
|
|
117
|
+
parser.add_argument("--strict", action="store_true", help="Exit non-zero when thresholds fail")
|
|
118
|
+
args = parser.parse_args()
|
|
119
|
+
if Image is None:
|
|
120
|
+
parser.error("Pillow is required for pixel comparison; use a workspace Python runtime that provides PIL.")
|
|
121
|
+
if bool(args.baseline_geometry) != bool(args.candidate_geometry):
|
|
122
|
+
parser.error("Provide both --baseline-geometry and --candidate-geometry")
|
|
123
|
+
|
|
124
|
+
image_report = compare_images(args.baseline, args.candidate, max(0, args.channel_tolerance), args.diff_image)
|
|
125
|
+
image_pass = image_report.get("status") == "pass" and image_report.get("changedRatio", 1) <= args.max_changed_ratio and image_report.get("meanAbsoluteError", float("inf")) <= args.max_mean_error
|
|
126
|
+
geometry_report = compare_geometry(args.baseline_geometry, args.candidate_geometry, max(0, args.geometry_tolerance)) if args.baseline_geometry else None
|
|
127
|
+
geometry_pass = geometry_report is None or geometry_report.get("status") == "pass"
|
|
128
|
+
report = {
|
|
129
|
+
"version": 1,
|
|
130
|
+
"status": "pass" if image_pass and geometry_pass else "fail",
|
|
131
|
+
"thresholds": {"maxChangedRatio": args.max_changed_ratio, "maxMeanAbsoluteError": args.max_mean_error, "geometryToleranceCssPx": args.geometry_tolerance},
|
|
132
|
+
"image": image_report,
|
|
133
|
+
"geometry": geometry_report,
|
|
134
|
+
}
|
|
135
|
+
write_json(args.output, report)
|
|
136
|
+
print(f"Visual regression: {report['status']} | changed={image_report.get('changedRatio', 1):.4%}")
|
|
137
|
+
return 2 if args.strict and report["status"] != "pass" else 0
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
if __name__ == "__main__":
|
|
141
|
+
sys.exit(main())
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ui-design-workbench-cli
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: Repository UI indexing, graph, preview, and deterministic review CLI
|
|
5
|
+
Author: Elgreed
|
|
6
|
+
Project-URL: Homepage, https://github.com/Elgreed/ui-design-workbench
|
|
7
|
+
Project-URL: Repository, https://github.com/Elgreed/ui-design-workbench
|
|
8
|
+
Project-URL: Changelog, https://github.com/Elgreed/ui-design-workbench/blob/main/CHANGELOG.md
|
|
9
|
+
Keywords: agent-skills,ui,ux,design-review,code-analysis
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
Provides-Extra: mcp
|
|
18
|
+
Requires-Dist: mcp<3,>=2; extra == "mcp"
|
|
19
|
+
|
|
20
|
+
# UI Design Workbench
|
|
21
|
+
|
|
22
|
+
[Русская версия](README.ru.md) · [Changelog](CHANGELOG.md) · [Release guide](RELEASING.md)
|
|
23
|
+
|
|
24
|
+
Turn repository UI source into an offline interactive HTML workbench—without building or running the application.
|
|
25
|
+
|
|
26
|
+
```text
|
|
27
|
+
UI source → cached UI map → strict UI IR → interactive HTML
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Use it to understand unfamiliar UI, inspect screens and navigation, verify a reconstruction, or run an explicit UI/UX review.
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
Requires Python 3.10+ and [`pipx`](https://pipx.pypa.io/).
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
pipx install ui-design-workbench-cli
|
|
38
|
+
uidw install-skill codex
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`install-skill` also supports `claude`, `cursor`, `gemini`, `copilot`, `opencode`, `agents`, and `all`.
|
|
42
|
+
|
|
43
|
+
Optional local MCP support:
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
pipx inject ui-design-workbench-cli "mcp>=2,<3"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Verify the installation:
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
uidw --version
|
|
53
|
+
uidw doctor
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Quick start
|
|
57
|
+
|
|
58
|
+
Choose preview detail once:
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
uidw --repo <repo> config setup
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Build and open the source-derived workbench:
|
|
65
|
+
|
|
66
|
+
```sh
|
|
67
|
+
uidw --repo <repo> workbench --output-dir <artifacts> --level full --open
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Run a UI/UX audit only when product critique is wanted:
|
|
71
|
+
|
|
72
|
+
```sh
|
|
73
|
+
uidw --repo <repo> review --output-dir <review-dir> --level full
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`workbench` and `check` validate the projection. Only `review` creates UI/UX findings.
|
|
77
|
+
|
|
78
|
+
## What it provides
|
|
79
|
+
|
|
80
|
+
- Screen, route, component, token, theme, and state inventory.
|
|
81
|
+
- Standalone HTML with source links and reconstructed navigation.
|
|
82
|
+
- Incremental scans that reprocess only changed UI files.
|
|
83
|
+
- Property-level evidence and explicit unsupported gaps instead of guessed UI.
|
|
84
|
+
- Sparse review patches and a separate, authorized source-apply step.
|
|
85
|
+
- Structural Android and Apple resource projection, plus discovery of available native capture paths.
|
|
86
|
+
|
|
87
|
+
Supported source families include Web, React, Vue, Svelte, Jetpack Compose, Android Views XML, SwiftUI, Storyboard/XIB, WinUI/WPF, and Flutter.
|
|
88
|
+
|
|
89
|
+
## Main commands
|
|
90
|
+
|
|
91
|
+
| Command | Purpose |
|
|
92
|
+
| --- | --- |
|
|
93
|
+
| `uidw doctor` | Check installation and optional dependencies |
|
|
94
|
+
| `uidw --repo <repo> context --json` | Read compact cached project context |
|
|
95
|
+
| `uidw --repo <repo> scope ...` | Prepare bounded context for a screen or finding |
|
|
96
|
+
| `uidw --repo <repo> patch ...` | Validate or apply sparse review-artifact changes |
|
|
97
|
+
| `uidw --repo <repo> workbench ...` | Build and validate the HTML projection |
|
|
98
|
+
| `uidw --repo <repo> native status` | Discover native Android/Apple render providers without running them |
|
|
99
|
+
| `uidw --repo <repo> check ...` | Repeat projection checks without a UI/UX audit |
|
|
100
|
+
| `uidw --repo <repo> review ...` | Start an explicit UI/UX review |
|
|
101
|
+
| `uidw --repo <repo> fidelity ...` | Inspect source evidence and adapter limits |
|
|
102
|
+
| `uidw --repo <repo> mcp` | Start the optional local stdio MCP server |
|
|
103
|
+
|
|
104
|
+
Run `uidw help overview`, `uidw help advanced`, or `uidw <command> --help` for details.
|
|
105
|
+
|
|
106
|
+
## Accuracy and safety
|
|
107
|
+
|
|
108
|
+
- HTML is a static source projection, not proof of runtime or pixel parity.
|
|
109
|
+
- Android and Apple translation remains structural until a source-matched native capture exists.
|
|
110
|
+
- Unsupported bindings, custom drawing, runtime data, and platform behavior remain explicit gaps.
|
|
111
|
+
- Preview and review keep application source read-only; applying a proposal is a separate step.
|
|
112
|
+
- Derived cache and review state live outside the target repository by default.
|
|
113
|
+
|
|
114
|
+
## Upgrade
|
|
115
|
+
|
|
116
|
+
```sh
|
|
117
|
+
pipx upgrade ui-design-workbench-cli
|
|
118
|
+
uidw install-skill codex
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
A Windows `.exe` is not published yet. PyPI + `pipx` is the primary cross-platform installation path.
|
|
122
|
+
|
|
123
|
+
## Documentation
|
|
124
|
+
|
|
125
|
+
- [Changelog](CHANGELOG.md)
|
|
126
|
+
- [Release and distribution guide](RELEASING.md)
|
|
127
|
+
- [Agent integrations](references/agent-integrations.md)
|
|
128
|
+
- [Fidelity contract](references/fidelity.md)
|
|
129
|
+
- [Native rendering](references/native-rendering.md)
|
|
130
|
+
- [IR schema](references/ir-schema.md)
|
|
131
|
+
- [Review workflow](references/review-workflow.md)
|
|
132
|
+
|
|
133
|
+
Current CLI version: `0.5.0`.
|