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
fidelity_core.py
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Deterministic fidelity primitives shared by scanners, adapters and CLI gates."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import copy
|
|
7
|
+
import hashlib
|
|
8
|
+
import json
|
|
9
|
+
import re
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Any, Iterable
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
FIDELITY_SCHEMA_VERSION = "0.3"
|
|
15
|
+
CONFIDENCE_LEVELS = {"exact", "high", "approximate", "unsupported"}
|
|
16
|
+
NODE_TYPES = {"container", "card", "text", "button", "input", "image", "icon", "spacer", "divider", "list", "custom"}
|
|
17
|
+
BASELINE_FIELDS = ("screens", "screenTree", "nodes", "tokens", "themes", "scenarioFixtures")
|
|
18
|
+
VISUAL_PREFIXES = ("layout.", "style.", "text", "asset", "component", "action")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def canonical_json(value: Any) -> str:
|
|
22
|
+
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def stable_id(*parts: Any, prefix: str = "evidence") -> str:
|
|
26
|
+
digest = hashlib.sha256("\x1f".join(str(part) for part in parts).encode("utf-8")).hexdigest()[:16]
|
|
27
|
+
return f"{prefix}-{digest}"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def property_evidence(file: str, line: int, expression: str, adapter: str, confidence: str = "exact") -> dict[str, Any]:
|
|
31
|
+
return {
|
|
32
|
+
"id": stable_id(file, line, expression, adapter),
|
|
33
|
+
"kind": "source",
|
|
34
|
+
"file": file,
|
|
35
|
+
"line": max(1, int(line or 1)),
|
|
36
|
+
"expression": expression.strip()[:500],
|
|
37
|
+
"adapter": adapter,
|
|
38
|
+
"confidence": confidence if confidence in CONFIDENCE_LEVELS else "approximate",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def baseline_payload(ir: dict[str, Any]) -> dict[str, Any]:
|
|
43
|
+
return {field: copy.deepcopy(ir.get(field, {} if field in {"nodes", "tokens", "themes", "scenarioFixtures"} else [])) for field in BASELINE_FIELDS}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def baseline_hash(ir: dict[str, Any]) -> str:
|
|
47
|
+
return "sha256:" + hashlib.sha256(canonical_json(baseline_payload(ir)).encode("utf-8")).hexdigest()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def seal_baseline(ir: dict[str, Any]) -> str:
|
|
51
|
+
review = ir.setdefault("review", {})
|
|
52
|
+
versions = review.setdefault("versions", [])
|
|
53
|
+
baseline_id = str(review.get("baselineVersion") or "baseline")
|
|
54
|
+
review["baselineVersion"] = baseline_id
|
|
55
|
+
baseline = next((item for item in versions if isinstance(item, dict) and item.get("id") == baseline_id), None)
|
|
56
|
+
if baseline is None:
|
|
57
|
+
baseline = {"id": baseline_id, "label": "Before", "kind": "baseline", "status": "approved", "nodeOverrides": {}}
|
|
58
|
+
versions.insert(0, baseline)
|
|
59
|
+
baseline["kind"] = "baseline"
|
|
60
|
+
baseline["nodeOverrides"] = {}
|
|
61
|
+
digest = baseline_hash(ir)
|
|
62
|
+
review["baselineHash"] = digest
|
|
63
|
+
review["baselineHashAlgorithm"] = "sha256"
|
|
64
|
+
return digest
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def verify_baseline(ir: dict[str, Any]) -> tuple[bool, str, str]:
|
|
68
|
+
expected = str(ir.get("review", {}).get("baselineHash") or "")
|
|
69
|
+
actual = baseline_hash(ir)
|
|
70
|
+
return bool(expected) and expected == actual, expected, actual
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(frozen=True)
|
|
74
|
+
class TokenDiagnostic:
|
|
75
|
+
token: str
|
|
76
|
+
code: str
|
|
77
|
+
detail: str
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class TokenResolver:
|
|
81
|
+
"""Resolve nested `$group.token` aliases without erasing the original references."""
|
|
82
|
+
|
|
83
|
+
_REFERENCE = re.compile(r"^\$([A-Za-z0-9_.-]+)$")
|
|
84
|
+
|
|
85
|
+
def __init__(self, tokens: dict[str, Any], overrides: dict[str, Any] | None = None):
|
|
86
|
+
self.tokens = copy.deepcopy(tokens or {})
|
|
87
|
+
self.overrides = copy.deepcopy(overrides or {})
|
|
88
|
+
self.diagnostics: list[TokenDiagnostic] = []
|
|
89
|
+
self._flat = self._flatten(self.tokens)
|
|
90
|
+
self._flat.update(self._flatten(self.overrides))
|
|
91
|
+
|
|
92
|
+
@staticmethod
|
|
93
|
+
def _flatten(value: dict[str, Any], prefix: str = "") -> dict[str, Any]:
|
|
94
|
+
result: dict[str, Any] = {}
|
|
95
|
+
for key, item in value.items():
|
|
96
|
+
path = f"{prefix}.{key}" if prefix else str(key)
|
|
97
|
+
if isinstance(item, dict) and "value" not in item:
|
|
98
|
+
result.update(TokenResolver._flatten(item, path))
|
|
99
|
+
else:
|
|
100
|
+
result[path] = item.get("value") if isinstance(item, dict) and "value" in item else item
|
|
101
|
+
return result
|
|
102
|
+
|
|
103
|
+
def resolve(self, value: Any) -> Any:
|
|
104
|
+
return self._resolve(value, ())
|
|
105
|
+
|
|
106
|
+
def _resolve(self, value: Any, stack: tuple[str, ...]) -> Any:
|
|
107
|
+
if not isinstance(value, str):
|
|
108
|
+
return value
|
|
109
|
+
match = self._REFERENCE.match(value.strip())
|
|
110
|
+
if not match:
|
|
111
|
+
return value
|
|
112
|
+
name = match.group(1)
|
|
113
|
+
if name in stack:
|
|
114
|
+
self.diagnostics.append(TokenDiagnostic(name, "token-cycle", " -> ".join((*stack, name))))
|
|
115
|
+
return value
|
|
116
|
+
if name not in self._flat:
|
|
117
|
+
self.diagnostics.append(TokenDiagnostic(name, "token-missing", f"No token named {name}"))
|
|
118
|
+
return value
|
|
119
|
+
return self._resolve(self._flat[name], (*stack, name))
|
|
120
|
+
|
|
121
|
+
def resolved_tokens(self) -> dict[str, Any]:
|
|
122
|
+
return {name: self._resolve(value, (name,)) for name, value in sorted(self._flat.items())}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _path_exists(node: dict[str, Any], path: str) -> bool:
|
|
126
|
+
current: Any = node
|
|
127
|
+
for part in path.split("."):
|
|
128
|
+
if not isinstance(current, dict) or part not in current:
|
|
129
|
+
return False
|
|
130
|
+
current = current[part]
|
|
131
|
+
return True
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _property_paths(node: dict[str, Any]) -> Iterable[str]:
|
|
135
|
+
for group in ("layout", "style"):
|
|
136
|
+
for key in node.get(group, {}) if isinstance(node.get(group), dict) else ():
|
|
137
|
+
yield f"{group}.{key}"
|
|
138
|
+
for key in ("text", "asset", "component", "action"):
|
|
139
|
+
if key in node:
|
|
140
|
+
yield key
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def validate_strict_ir(ir: dict[str, Any]) -> list[str]:
|
|
144
|
+
"""Semantic v0.3 validator. It is strict only for fidelity schema 0.3 artifacts."""
|
|
145
|
+
if str(ir.get("fidelity", {}).get("schemaVersion") or "") != FIDELITY_SCHEMA_VERSION:
|
|
146
|
+
return []
|
|
147
|
+
errors: list[str] = []
|
|
148
|
+
nodes = ir.get("nodes")
|
|
149
|
+
screens = ir.get("screens")
|
|
150
|
+
if not isinstance(nodes, dict) or not nodes:
|
|
151
|
+
return ["Fidelity 0.3 IR requires non-empty nodes"]
|
|
152
|
+
if not isinstance(screens, list) or not screens:
|
|
153
|
+
errors.append("Fidelity 0.3 IR requires non-empty screens")
|
|
154
|
+
screens = []
|
|
155
|
+
root_ids = {screen.get("root") for screen in screens if isinstance(screen, dict)}
|
|
156
|
+
for root_id in sorted(root_ids):
|
|
157
|
+
if root_id not in nodes:
|
|
158
|
+
errors.append(f"Screen root does not exist: {root_id}")
|
|
159
|
+
for node_id, node in nodes.items():
|
|
160
|
+
if not isinstance(node, dict):
|
|
161
|
+
errors.append(f"Node {node_id} must be an object")
|
|
162
|
+
continue
|
|
163
|
+
if node.get("type") not in NODE_TYPES:
|
|
164
|
+
errors.append(f"Node {node_id} has unsupported type {node.get('type')}")
|
|
165
|
+
confidence = node.get("confidence")
|
|
166
|
+
if confidence not in CONFIDENCE_LEVELS:
|
|
167
|
+
errors.append(f"Node {node_id} needs exact/high/approximate/unsupported confidence")
|
|
168
|
+
provenance = node.get("provenance", {})
|
|
169
|
+
if not isinstance(provenance, dict):
|
|
170
|
+
errors.append(f"Node {node_id} provenance must be an object")
|
|
171
|
+
provenance = {}
|
|
172
|
+
for path, evidence in provenance.items():
|
|
173
|
+
if not _path_exists(node, path):
|
|
174
|
+
errors.append(f"Node {node_id} provenance references missing property {path}")
|
|
175
|
+
if not isinstance(evidence, dict) or not all(evidence.get(key) not in (None, "") for key in ("id", "kind", "file", "line", "adapter", "confidence")):
|
|
176
|
+
errors.append(f"Node {node_id} has invalid provenance for {path}")
|
|
177
|
+
elif evidence.get("confidence") not in CONFIDENCE_LEVELS:
|
|
178
|
+
errors.append(f"Node {node_id} provenance {path} has invalid confidence")
|
|
179
|
+
if confidence != "unsupported":
|
|
180
|
+
for path in _property_paths(node):
|
|
181
|
+
if path.startswith(VISUAL_PREFIXES) and path not in provenance:
|
|
182
|
+
errors.append(f"Node {node_id} property {path} lacks provenance")
|
|
183
|
+
for child in node.get("children", []):
|
|
184
|
+
if child not in nodes:
|
|
185
|
+
errors.append(f"Node {node_id} references missing child {child}")
|
|
186
|
+
resolver = TokenResolver(ir.get("tokens", {}))
|
|
187
|
+
for node in nodes.values():
|
|
188
|
+
if not isinstance(node, dict):
|
|
189
|
+
continue
|
|
190
|
+
for group in ("layout", "style"):
|
|
191
|
+
for value in node.get(group, {}).values() if isinstance(node.get(group), dict) else ():
|
|
192
|
+
resolver.resolve(value)
|
|
193
|
+
errors.extend(f"{item.code}: {item.detail}" for item in resolver.diagnostics)
|
|
194
|
+
for theme in ir.get("themes", {}).get("items", []) if isinstance(ir.get("themes"), dict) else ():
|
|
195
|
+
if not isinstance(theme, dict):
|
|
196
|
+
continue
|
|
197
|
+
theme_resolver = TokenResolver(ir.get("tokens", {}), theme.get("tokenOverrides", {}))
|
|
198
|
+
theme_resolver.resolved_tokens()
|
|
199
|
+
errors.extend(f"theme {theme.get('id')}: {item.code}: {item.detail}" for item in theme_resolver.diagnostics)
|
|
200
|
+
valid_baseline, expected, actual = verify_baseline(ir)
|
|
201
|
+
if not valid_baseline:
|
|
202
|
+
errors.append(f"Immutable baseline hash mismatch: expected {expected or '<missing>'}, actual {actual}")
|
|
203
|
+
return errors
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def fidelity_report(ir: dict[str, Any]) -> dict[str, Any]:
|
|
207
|
+
schema_version = str(ir.get("fidelity", {}).get("schemaVersion") or "")
|
|
208
|
+
applicable = schema_version == FIDELITY_SCHEMA_VERSION
|
|
209
|
+
errors = validate_strict_ir(ir)
|
|
210
|
+
properties = 0
|
|
211
|
+
sourced = 0
|
|
212
|
+
by_confidence = {level: 0 for level in sorted(CONFIDENCE_LEVELS)}
|
|
213
|
+
unsupported: list[dict[str, Any]] = []
|
|
214
|
+
for node_id, node in ir.get("nodes", {}).items():
|
|
215
|
+
if not isinstance(node, dict):
|
|
216
|
+
continue
|
|
217
|
+
by_confidence[str(node.get("confidence") or "unsupported")] = by_confidence.get(str(node.get("confidence") or "unsupported"), 0) + 1
|
|
218
|
+
provenance = node.get("provenance", {}) if isinstance(node.get("provenance"), dict) else {}
|
|
219
|
+
for path in _property_paths(node):
|
|
220
|
+
properties += 1
|
|
221
|
+
sourced += int(path in provenance)
|
|
222
|
+
if node.get("confidence") == "unsupported":
|
|
223
|
+
unsupported.append({"nodeId": node_id, "component": node.get("component"), "source": node.get("source", {})})
|
|
224
|
+
baseline_valid, baseline_expected, baseline_actual = verify_baseline(ir)
|
|
225
|
+
fidelity_errors = [
|
|
226
|
+
*errors,
|
|
227
|
+
*(f"Node {item['nodeId']} is an unsupported placeholder" for item in unsupported),
|
|
228
|
+
]
|
|
229
|
+
return {
|
|
230
|
+
"version": 1,
|
|
231
|
+
"schemaVersion": schema_version or None,
|
|
232
|
+
"applicable": applicable,
|
|
233
|
+
"status": "not-applicable" if not applicable else "pass" if not fidelity_errors else "fail",
|
|
234
|
+
"applicabilityReason": None if applicable else f"Fidelity Core requires schema {FIDELITY_SCHEMA_VERSION}",
|
|
235
|
+
"strictErrors": errors,
|
|
236
|
+
"fidelityErrors": fidelity_errors,
|
|
237
|
+
"propertyProvenance": {"covered": sourced, "total": properties, "percent": round(100 * sourced / properties, 1) if properties else 100.0},
|
|
238
|
+
"nodeConfidence": by_confidence,
|
|
239
|
+
"unsupported": unsupported,
|
|
240
|
+
"baseline": {"valid": baseline_valid, "expected": baseline_expected, "actual": baseline_actual},
|
|
241
|
+
}
|