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
scoped_context.py
ADDED
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Bounded agent context and safe sparse patches for UI Design Workbench IR."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import copy
|
|
7
|
+
import hashlib
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
CONTEXT_TYPE = "ui-design-workbench-scoped-context"
|
|
16
|
+
PATCH_TYPE = "ui-design-workbench-ir-patch"
|
|
17
|
+
FORMAT_VERSION = 1
|
|
18
|
+
TOKEN_REF = re.compile(r"\$([A-Za-z0-9_.-]+)")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _compact(value: Any) -> str:
|
|
22
|
+
return json.dumps(value, ensure_ascii=False, separators=(",", ":"), sort_keys=True)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def estimate_tokens(value: Any) -> int:
|
|
26
|
+
"""Return a conservative, dependency-free estimate for routing/budget metadata."""
|
|
27
|
+
return max(1, (len(_compact(value).encode("utf-8")) + 3) // 4)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def read_json(path: Path) -> dict[str, Any]:
|
|
31
|
+
try:
|
|
32
|
+
value = json.loads(path.read_text(encoding="utf-8"))
|
|
33
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
34
|
+
raise ValueError(f"Cannot read JSON: {path}") from exc
|
|
35
|
+
if not isinstance(value, dict):
|
|
36
|
+
raise ValueError(f"Expected a JSON object: {path}")
|
|
37
|
+
return value
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def write_json(path: Path, value: Any) -> None:
|
|
41
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
42
|
+
temporary = path.with_name(f".{path.name}.{os.getpid()}.tmp")
|
|
43
|
+
try:
|
|
44
|
+
temporary.write_text(json.dumps(value, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
45
|
+
os.replace(temporary, path)
|
|
46
|
+
finally:
|
|
47
|
+
try:
|
|
48
|
+
temporary.unlink()
|
|
49
|
+
except OSError:
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _ordered_findings(ir: dict[str, Any]) -> list[dict[str, Any]]:
|
|
54
|
+
findings = ir.get("review", {}).get("audit", {}).get("findings", [])
|
|
55
|
+
return [item for item in findings if isinstance(item, dict) and item.get("id")]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _resolve_screens(ir: dict[str, Any], queries: list[str]) -> list[dict[str, Any]]:
|
|
59
|
+
screens = [item for item in ir.get("screens", []) if isinstance(item, dict)]
|
|
60
|
+
if not queries:
|
|
61
|
+
return []
|
|
62
|
+
normalized = {item.strip().lower() for item in queries if item.strip()}
|
|
63
|
+
selected = [
|
|
64
|
+
screen for screen in screens
|
|
65
|
+
if str(screen.get("id", "")).lower() in normalized
|
|
66
|
+
or str(screen.get("name", "")).lower() in normalized
|
|
67
|
+
]
|
|
68
|
+
missing = normalized - {
|
|
69
|
+
value
|
|
70
|
+
for item in selected
|
|
71
|
+
for value in (str(item.get("id", "")).lower(), str(item.get("name", "")).lower())
|
|
72
|
+
}
|
|
73
|
+
if missing:
|
|
74
|
+
raise ValueError("Unknown screen(s): " + ", ".join(sorted(missing)))
|
|
75
|
+
return selected
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _resolve_findings(ir: dict[str, Any], identifiers: list[str]) -> list[dict[str, Any]]:
|
|
79
|
+
findings = _ordered_findings(ir)
|
|
80
|
+
by_id = {str(item["id"]): item for item in findings}
|
|
81
|
+
by_number = {str(index): item for index, item in enumerate(findings, 1)}
|
|
82
|
+
selected: list[dict[str, Any]] = []
|
|
83
|
+
missing: list[str] = []
|
|
84
|
+
for identifier in identifiers:
|
|
85
|
+
item = by_id.get(identifier) or by_number.get(identifier)
|
|
86
|
+
if item and item not in selected:
|
|
87
|
+
selected.append(item)
|
|
88
|
+
elif not item:
|
|
89
|
+
missing.append(identifier)
|
|
90
|
+
if missing:
|
|
91
|
+
raise ValueError("Unknown finding(s): " + ", ".join(missing))
|
|
92
|
+
return selected
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _node_ids_for_scope(
|
|
96
|
+
ir: dict[str, Any], screens: list[dict[str, Any]], findings: list[dict[str, Any]]
|
|
97
|
+
) -> set[str]:
|
|
98
|
+
nodes = ir.get("nodes", {}) if isinstance(ir.get("nodes"), dict) else {}
|
|
99
|
+
selected: set[str] = set()
|
|
100
|
+
parents: dict[str, str] = {}
|
|
101
|
+
for node_id, node in nodes.items():
|
|
102
|
+
if not isinstance(node, dict):
|
|
103
|
+
continue
|
|
104
|
+
for child in node.get("children", []):
|
|
105
|
+
parents[str(child)] = str(node_id)
|
|
106
|
+
|
|
107
|
+
def descendants(node_id: str) -> None:
|
|
108
|
+
if node_id in selected or node_id not in nodes:
|
|
109
|
+
return
|
|
110
|
+
selected.add(node_id)
|
|
111
|
+
node = nodes[node_id]
|
|
112
|
+
if isinstance(node, dict):
|
|
113
|
+
for child in node.get("children", []):
|
|
114
|
+
descendants(str(child))
|
|
115
|
+
|
|
116
|
+
for screen in screens:
|
|
117
|
+
descendants(str(screen.get("root", "")))
|
|
118
|
+
for finding in findings:
|
|
119
|
+
node_id = str(finding.get("nodeId") or "")
|
|
120
|
+
if node_id:
|
|
121
|
+
descendants(node_id)
|
|
122
|
+
while node_id in parents:
|
|
123
|
+
node_id = parents[node_id]
|
|
124
|
+
selected.add(node_id)
|
|
125
|
+
return selected
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _source_files(items: list[dict[str, Any]]) -> list[str]:
|
|
129
|
+
files: set[str] = set()
|
|
130
|
+
for item in items:
|
|
131
|
+
source = item.get("source")
|
|
132
|
+
if isinstance(source, dict) and source.get("file"):
|
|
133
|
+
files.add(str(source["file"]))
|
|
134
|
+
for evidence in item.get("evidence", []):
|
|
135
|
+
if isinstance(evidence, dict) and evidence.get("ref"):
|
|
136
|
+
files.add(str(evidence["ref"]).split("#", 1)[0])
|
|
137
|
+
return sorted(files)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _token_refs(value: Any) -> set[str]:
|
|
141
|
+
return set(TOKEN_REF.findall(_compact(value)))
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _lookup_token(tokens: dict[str, Any], path: str) -> Any:
|
|
145
|
+
current: Any = tokens
|
|
146
|
+
for segment in path.split("."):
|
|
147
|
+
if not isinstance(current, dict) or segment not in current:
|
|
148
|
+
return None
|
|
149
|
+
current = current[segment]
|
|
150
|
+
return current
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _set_token(target: dict[str, Any], path: str, value: Any) -> None:
|
|
154
|
+
current = target
|
|
155
|
+
parts = path.split(".")
|
|
156
|
+
for segment in parts[:-1]:
|
|
157
|
+
current = current.setdefault(segment, {})
|
|
158
|
+
current[parts[-1]] = copy.deepcopy(value)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _relevant_tokens(ir: dict[str, Any], selected_nodes: dict[str, Any]) -> dict[str, Any]:
|
|
162
|
+
tokens = ir.get("tokens", {}) if isinstance(ir.get("tokens"), dict) else {}
|
|
163
|
+
return _select_tokens(tokens, _token_refs(selected_nodes))
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _select_tokens(tokens: dict[str, Any], refs: set[str]) -> dict[str, Any]:
|
|
167
|
+
pending = list(refs)
|
|
168
|
+
seen: set[str] = set()
|
|
169
|
+
result: dict[str, Any] = {}
|
|
170
|
+
while pending:
|
|
171
|
+
ref = pending.pop()
|
|
172
|
+
if ref in seen:
|
|
173
|
+
continue
|
|
174
|
+
seen.add(ref)
|
|
175
|
+
value = _lookup_token(tokens, ref)
|
|
176
|
+
if value is None:
|
|
177
|
+
continue
|
|
178
|
+
_set_token(result, ref, value)
|
|
179
|
+
pending.extend(_token_refs(value) - seen)
|
|
180
|
+
return result
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _relevant_themes(ir: dict[str, Any], selected_nodes: dict[str, Any], node_ids: set[str]) -> dict[str, Any]:
|
|
184
|
+
themes = ir.get("themes", {}) if isinstance(ir.get("themes"), dict) else {}
|
|
185
|
+
refs = _token_refs(selected_nodes)
|
|
186
|
+
items: list[dict[str, Any]] = []
|
|
187
|
+
for item in themes.get("items", []) if isinstance(themes.get("items"), list) else []:
|
|
188
|
+
if not isinstance(item, dict):
|
|
189
|
+
continue
|
|
190
|
+
selected = {key: copy.deepcopy(value) for key, value in item.items() if key in {"id", "label", "kind", "sourceRefs"}}
|
|
191
|
+
token_overrides = item.get("tokenOverrides", {}) if isinstance(item.get("tokenOverrides"), dict) else {}
|
|
192
|
+
node_overrides = item.get("nodeOverrides", {}) if isinstance(item.get("nodeOverrides"), dict) else {}
|
|
193
|
+
relevant_tokens = _select_tokens(token_overrides, refs)
|
|
194
|
+
relevant_nodes = {key: copy.deepcopy(value) for key, value in node_overrides.items() if str(key) in node_ids}
|
|
195
|
+
if relevant_tokens:
|
|
196
|
+
selected["tokenOverrides"] = relevant_tokens
|
|
197
|
+
if relevant_nodes:
|
|
198
|
+
selected["nodeOverrides"] = relevant_nodes
|
|
199
|
+
items.append(selected)
|
|
200
|
+
return {"defaultThemeId": themes.get("defaultThemeId"), "items": items}
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _relevant_fixtures(ir: dict[str, Any], screens: list[dict[str, Any]]) -> dict[str, Any]:
|
|
204
|
+
fixtures = ir.get("scenarioFixtures", {}) if isinstance(ir.get("scenarioFixtures"), dict) else {}
|
|
205
|
+
refs = {
|
|
206
|
+
str(scenario.get("fixtureRef"))
|
|
207
|
+
for screen in screens
|
|
208
|
+
for scenario in screen.get("scenarios", [])
|
|
209
|
+
if isinstance(scenario, dict) and scenario.get("fixtureRef")
|
|
210
|
+
}
|
|
211
|
+
return {key: copy.deepcopy(value) for key, value in fixtures.items() if str(key) in refs}
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _relevant_navigation(ir: dict[str, Any], screen_ids: set[str]) -> dict[str, Any]:
|
|
215
|
+
graph = ir.get("navigationGraph", {}) if isinstance(ir.get("navigationGraph"), dict) else {}
|
|
216
|
+
edges = [
|
|
217
|
+
copy.deepcopy(item) for item in graph.get("edges", [])
|
|
218
|
+
if isinstance(item, dict) and (str(item.get("from")) in screen_ids or str(item.get("to")) in screen_ids)
|
|
219
|
+
]
|
|
220
|
+
targets = [
|
|
221
|
+
copy.deepcopy(item) for item in graph.get("navigationTargets", [])
|
|
222
|
+
if isinstance(item, dict) and any(str(item.get(key)) in screen_ids for key in ("screenId", "from", "to", "target"))
|
|
223
|
+
]
|
|
224
|
+
return {"edges": edges, "navigationTargets": targets}
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _relevant_components(ir: dict[str, Any], nodes: dict[str, Any]) -> list[Any]:
|
|
228
|
+
refs = {str(node.get("componentRef")) for node in nodes.values() if isinstance(node, dict) and node.get("componentRef")}
|
|
229
|
+
catalog = ir.get("componentCatalog", [])
|
|
230
|
+
if isinstance(catalog, dict):
|
|
231
|
+
return [{"id": key, **copy.deepcopy(value)} if isinstance(value, dict) else {"id": key, "value": value} for key, value in catalog.items() if str(key) in refs]
|
|
232
|
+
return [copy.deepcopy(item) for item in catalog if isinstance(item, dict) and str(item.get("id")) in refs]
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _relevant_versions(ir: dict[str, Any], node_ids: set[str], finding_ids: set[str]) -> list[dict[str, Any]]:
|
|
236
|
+
result: list[dict[str, Any]] = []
|
|
237
|
+
versions = ir.get("review", {}).get("versions", [])
|
|
238
|
+
for version in versions if isinstance(versions, list) else []:
|
|
239
|
+
if not isinstance(version, dict):
|
|
240
|
+
continue
|
|
241
|
+
covered = set(map(str, version.get("findingIds", []))) | set(map(str, version.get("resolvedFindingIds", [])))
|
|
242
|
+
overrides = version.get("nodeOverrides", {}) if isinstance(version.get("nodeOverrides"), dict) else {}
|
|
243
|
+
relevant_overrides = {key: copy.deepcopy(value) for key, value in overrides.items() if str(key) in node_ids}
|
|
244
|
+
if covered.intersection(finding_ids) or relevant_overrides:
|
|
245
|
+
sparse = {key: copy.deepcopy(value) for key, value in version.items() if key != "nodeOverrides"}
|
|
246
|
+
sparse["nodeOverrides"] = relevant_overrides
|
|
247
|
+
result.append(sparse)
|
|
248
|
+
return result
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _compact_finding(item: dict[str, Any]) -> dict[str, Any]:
|
|
252
|
+
keep = {
|
|
253
|
+
"id", "title", "severity", "category", "screenId", "nodeId", "reviewVersionId",
|
|
254
|
+
"observation", "impact", "recommendation", "confidence", "sourceTarget",
|
|
255
|
+
"runtimeDiagnosticId", "verification", "proposalVersionId", "status",
|
|
256
|
+
}
|
|
257
|
+
return {key: copy.deepcopy(value) for key, value in item.items() if key in keep}
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _scope_catalog(ir: dict[str, Any]) -> dict[str, Any]:
|
|
261
|
+
return {
|
|
262
|
+
"screens": [
|
|
263
|
+
{key: item.get(key) for key in ("id", "name", "platform", "route", "fragment") if item.get(key) is not None}
|
|
264
|
+
for item in ir.get("screens", []) if isinstance(item, dict)
|
|
265
|
+
],
|
|
266
|
+
"findings": [
|
|
267
|
+
{key: item.get(key) for key in ("id", "title", "severity", "screenId", "nodeId") if item.get(key) is not None}
|
|
268
|
+
for item in _ordered_findings(ir)
|
|
269
|
+
],
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def build_scoped_context(
|
|
274
|
+
ir: dict[str, Any],
|
|
275
|
+
*,
|
|
276
|
+
screen_ids: list[str] | None = None,
|
|
277
|
+
finding_ids: list[str] | None = None,
|
|
278
|
+
token_budget: int = 4000,
|
|
279
|
+
ui_ir_file: str | None = None,
|
|
280
|
+
) -> dict[str, Any]:
|
|
281
|
+
"""Build a structurally complete scope. It never cuts a selected node subtree."""
|
|
282
|
+
screen_ids = screen_ids or []
|
|
283
|
+
finding_ids = finding_ids or []
|
|
284
|
+
findings = _resolve_findings(ir, finding_ids)
|
|
285
|
+
inferred_screens = [str(item.get("screenId")) for item in findings if item.get("screenId")]
|
|
286
|
+
screens = _resolve_screens(ir, list(dict.fromkeys([*screen_ids, *inferred_screens])))
|
|
287
|
+
node_ids = _node_ids_for_scope(ir, screens, findings)
|
|
288
|
+
all_nodes = ir.get("nodes", {}) if isinstance(ir.get("nodes"), dict) else {}
|
|
289
|
+
nodes = {node_id: copy.deepcopy(all_nodes[node_id]) for node_id in all_nodes if node_id in node_ids}
|
|
290
|
+
review = ir.get("review", {}) if isinstance(ir.get("review"), dict) else {}
|
|
291
|
+
project = ir.get("project", {}) if isinstance(ir.get("project"), dict) else {}
|
|
292
|
+
payload: dict[str, Any] = {
|
|
293
|
+
"type": CONTEXT_TYPE,
|
|
294
|
+
"version": FORMAT_VERSION,
|
|
295
|
+
"project": {key: project.get(key) for key in ("name", "root") if project.get(key) is not None},
|
|
296
|
+
"target": {
|
|
297
|
+
"uiIrFile": ui_ir_file,
|
|
298
|
+
"baselineHash": review.get("baselineHash"),
|
|
299
|
+
"reviewRevision": review.get("revision"),
|
|
300
|
+
"baselineVersion": review.get("baselineVersion"),
|
|
301
|
+
},
|
|
302
|
+
"scope": {
|
|
303
|
+
"screenIds": [str(item.get("id")) for item in screens],
|
|
304
|
+
"findingIds": [str(item.get("id")) for item in findings],
|
|
305
|
+
"nodeIds": list(nodes),
|
|
306
|
+
},
|
|
307
|
+
"screens": copy.deepcopy(screens),
|
|
308
|
+
"nodes": nodes,
|
|
309
|
+
"findings": [_compact_finding(item) for item in findings],
|
|
310
|
+
"versions": _relevant_versions(ir, node_ids, {str(item.get("id")) for item in findings}),
|
|
311
|
+
"tokens": _relevant_tokens(ir, nodes),
|
|
312
|
+
"themes": _relevant_themes(ir, nodes, node_ids),
|
|
313
|
+
"scenarioFixtures": _relevant_fixtures(ir, screens),
|
|
314
|
+
"navigation": _relevant_navigation(ir, {str(item.get("id")) for item in screens}),
|
|
315
|
+
"components": _relevant_components(ir, nodes),
|
|
316
|
+
"platforms": copy.deepcopy(ir.get("platforms", [])),
|
|
317
|
+
"sourceFiles": _source_files([*screens, *nodes.values(), *findings]),
|
|
318
|
+
"patchContract": {
|
|
319
|
+
"type": PATCH_TYPE,
|
|
320
|
+
"version": FORMAT_VERSION,
|
|
321
|
+
"allowedOperations": ["upsert-findings", "upsert-versions", "merge-annotations", "record-verifications"],
|
|
322
|
+
"forbidden": ["baseline screens", "baseline nodes", "baseline tokens", "baseline themes", "application source"],
|
|
323
|
+
},
|
|
324
|
+
}
|
|
325
|
+
if not screens and not findings:
|
|
326
|
+
payload["catalog"] = _scope_catalog(ir)
|
|
327
|
+
payload["next"] = "Request one screen or a small finding set; do not read the full ui-ir.json."
|
|
328
|
+
requested = max(256, int(token_budget))
|
|
329
|
+
estimated = estimate_tokens(payload)
|
|
330
|
+
payload["contextBudget"] = {
|
|
331
|
+
"requestedTokens": requested,
|
|
332
|
+
"estimatedTokens": estimated,
|
|
333
|
+
"withinBudget": estimated <= requested,
|
|
334
|
+
"structuralTruncation": False,
|
|
335
|
+
"recommendation": None if estimated <= requested else "Narrow screenIds/findingIds; selected structure was preserved intact.",
|
|
336
|
+
}
|
|
337
|
+
return payload
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def write_scoped_context(
|
|
341
|
+
ir_path: Path,
|
|
342
|
+
output: Path,
|
|
343
|
+
*,
|
|
344
|
+
screen_ids: list[str] | None = None,
|
|
345
|
+
finding_ids: list[str] | None = None,
|
|
346
|
+
token_budget: int = 4000,
|
|
347
|
+
) -> dict[str, Any]:
|
|
348
|
+
ir_path = ir_path.resolve()
|
|
349
|
+
payload = build_scoped_context(
|
|
350
|
+
read_json(ir_path), screen_ids=screen_ids, finding_ids=finding_ids,
|
|
351
|
+
token_budget=token_budget, ui_ir_file=str(ir_path),
|
|
352
|
+
)
|
|
353
|
+
write_json(output.resolve(), payload)
|
|
354
|
+
return payload
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def patch_template(ir: dict[str, Any], ui_ir_file: str, context_file: str | None = None) -> dict[str, Any]:
|
|
358
|
+
review = ir.get("review", {}) if isinstance(ir.get("review"), dict) else {}
|
|
359
|
+
return {
|
|
360
|
+
"type": PATCH_TYPE,
|
|
361
|
+
"version": FORMAT_VERSION,
|
|
362
|
+
"target": {
|
|
363
|
+
"uiIrFile": ui_ir_file,
|
|
364
|
+
"project": ir.get("project", {}).get("name"),
|
|
365
|
+
"baselineHash": review.get("baselineHash"),
|
|
366
|
+
"reviewRevision": review.get("revision"),
|
|
367
|
+
},
|
|
368
|
+
"contextFile": context_file,
|
|
369
|
+
"operations": [],
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def validate_patch(ir: dict[str, Any], patch: dict[str, Any]) -> list[str]:
|
|
374
|
+
errors: list[str] = []
|
|
375
|
+
if patch.get("type") != PATCH_TYPE or patch.get("version") != FORMAT_VERSION:
|
|
376
|
+
errors.append("Unsupported ui-ir.patch.json format")
|
|
377
|
+
target = patch.get("target", {}) if isinstance(patch.get("target"), dict) else {}
|
|
378
|
+
review = ir.get("review", {}) if isinstance(ir.get("review"), dict) else {}
|
|
379
|
+
project = ir.get("project", {}) if isinstance(ir.get("project"), dict) else {}
|
|
380
|
+
for label, expected, actual in (
|
|
381
|
+
("project", target.get("project"), project.get("name")),
|
|
382
|
+
("baselineHash", target.get("baselineHash"), review.get("baselineHash")),
|
|
383
|
+
("reviewRevision", target.get("reviewRevision"), review.get("revision")),
|
|
384
|
+
):
|
|
385
|
+
if expected is not None and expected != actual:
|
|
386
|
+
errors.append(f"Patch {label} does not match target IR")
|
|
387
|
+
allowed = {"upsert-findings", "upsert-versions", "merge-annotations", "record-verifications"}
|
|
388
|
+
operations = patch.get("operations")
|
|
389
|
+
if not isinstance(operations, list):
|
|
390
|
+
errors.append("Patch operations must be an array")
|
|
391
|
+
return errors
|
|
392
|
+
for index, operation in enumerate(operations):
|
|
393
|
+
if not isinstance(operation, dict) or operation.get("op") not in allowed:
|
|
394
|
+
errors.append(f"Unsupported operation at index {index}")
|
|
395
|
+
continue
|
|
396
|
+
if operation.get("op") in {"upsert-findings", "upsert-versions", "merge-annotations", "record-verifications"} and not isinstance(operation.get("value"), list):
|
|
397
|
+
errors.append(f"Operation {index} value must be an array")
|
|
398
|
+
if operation.get("op") == "upsert-versions":
|
|
399
|
+
for version in operation.get("value", []):
|
|
400
|
+
if not isinstance(version, dict) or version.get("kind") != "proposal" or not version.get("id"):
|
|
401
|
+
errors.append("Only named sparse proposal versions may be upserted")
|
|
402
|
+
if isinstance(version, dict) and any(key in version for key in ("screens", "nodes", "tokens", "themes")):
|
|
403
|
+
errors.append("Proposal operation cannot replace baseline collections")
|
|
404
|
+
return errors
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def apply_patch(ir: dict[str, Any], patch: dict[str, Any]) -> dict[str, Any]:
|
|
408
|
+
errors = validate_patch(ir, patch)
|
|
409
|
+
if errors:
|
|
410
|
+
raise ValueError("; ".join(errors))
|
|
411
|
+
result = copy.deepcopy(ir)
|
|
412
|
+
review = result.setdefault("review", {})
|
|
413
|
+
audit = review.setdefault("audit", {})
|
|
414
|
+
for operation in patch.get("operations", []):
|
|
415
|
+
op = operation["op"]
|
|
416
|
+
values = operation["value"]
|
|
417
|
+
if op == "upsert-findings":
|
|
418
|
+
existing = {str(item.get("id")): item for item in audit.setdefault("findings", []) if isinstance(item, dict) and item.get("id")}
|
|
419
|
+
for item in values:
|
|
420
|
+
if not isinstance(item, dict) or not item.get("id"):
|
|
421
|
+
raise ValueError("Every finding must have a stable id")
|
|
422
|
+
existing[str(item["id"])] = copy.deepcopy(item)
|
|
423
|
+
audit["findings"] = list(existing.values())
|
|
424
|
+
elif op == "upsert-versions":
|
|
425
|
+
existing = {str(item.get("id")): item for item in review.setdefault("versions", []) if isinstance(item, dict) and item.get("id")}
|
|
426
|
+
for item in values:
|
|
427
|
+
existing[str(item["id"])] = copy.deepcopy(item)
|
|
428
|
+
review["versions"] = list(existing.values())
|
|
429
|
+
elif op == "merge-annotations":
|
|
430
|
+
existing = {str(item.get("id")): item for item in review.setdefault("annotations", []) if isinstance(item, dict) and item.get("id")}
|
|
431
|
+
for item in values:
|
|
432
|
+
if not isinstance(item, dict) or not item.get("id"):
|
|
433
|
+
raise ValueError("Every annotation must have a stable id")
|
|
434
|
+
existing[str(item["id"])] = copy.deepcopy(item)
|
|
435
|
+
review["annotations"] = list(existing.values())
|
|
436
|
+
elif op == "record-verifications":
|
|
437
|
+
findings = {str(item.get("id")): item for item in audit.setdefault("findings", []) if isinstance(item, dict) and item.get("id")}
|
|
438
|
+
for item in values:
|
|
439
|
+
finding_id = str(item.get("findingId") or "") if isinstance(item, dict) else ""
|
|
440
|
+
if finding_id not in findings or not isinstance(item.get("verification"), dict):
|
|
441
|
+
raise ValueError(f"Invalid verification record for finding: {finding_id or '<missing>'}")
|
|
442
|
+
findings[finding_id]["verification"] = copy.deepcopy(item["verification"])
|
|
443
|
+
return result
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def apply_patch_file(ir_path: Path, patch_path: Path, output: Path) -> dict[str, Any]:
|
|
447
|
+
ir_path, patch_path, output = ir_path.resolve(), patch_path.resolve(), output.resolve()
|
|
448
|
+
source = read_json(ir_path)
|
|
449
|
+
patched = apply_patch(source, read_json(patch_path))
|
|
450
|
+
write_json(output, patched)
|
|
451
|
+
return {
|
|
452
|
+
"version": 1,
|
|
453
|
+
"status": "applied",
|
|
454
|
+
"irFile": str(output),
|
|
455
|
+
"patchFile": str(patch_path),
|
|
456
|
+
"baselineUnchanged": hashlib.sha256(_compact({key: source.get(key) for key in ("screens", "nodes", "tokens", "themes", "scenarioFixtures")}).encode()).hexdigest()
|
|
457
|
+
== hashlib.sha256(_compact({key: patched.get(key) for key in ("screens", "nodes", "tokens", "themes", "scenarioFixtures")}).encode()).hexdigest(),
|
|
458
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ui-design-workbench
|
|
3
|
+
description: Reconstruct, review, generate, or redesign repository UI as standards-aware interactive HTML previews; optionally guide ordinary UI implementation for Android, iOS, macOS, Windows, Flutter, or Web. Do not use when the user only wants to run the real application.
|
|
4
|
+
metadata:
|
|
5
|
+
compatibility: "Python 3.10+. MCP is optional; CLI works standalone."
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# UI Design Workbench router
|
|
9
|
+
|
|
10
|
+
Use UIDW as a deterministic local utility. The CLI, not the skill prompt, owns scanning, cache invalidation, source graphs, IR, rendering, and projection checks.
|
|
11
|
+
|
|
12
|
+
## 1. Get compact context
|
|
13
|
+
|
|
14
|
+
Prefer MCP tools `ui_project` then `ui_scope` when the local UIDW MCP is configured. Otherwise use the complete CLI fallback:
|
|
15
|
+
|
|
16
|
+
```text
|
|
17
|
+
uidw --repo <repo> context --json
|
|
18
|
+
uidw --repo <repo> scope --ir <ui-ir.json> --screen <id> --budget 4000
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
If `uidw` is unavailable, run `python <skill-dir>/scripts/uidw.py ...`. Reuse a clean cache. Never paste or read the complete `ui-ir.json` when a scoped context can answer the task. Request another screen/finding scope only when needed.
|
|
22
|
+
|
|
23
|
+
If first-use context says `setupRequired`, ask only for `low`, `medium`, or `high` using its short descriptions. Mock-data depth follows that choice. Do not recommend a level. Optional `uiMode` remains off until explicitly enabled.
|
|
24
|
+
|
|
25
|
+
## 2. Route the request
|
|
26
|
+
|
|
27
|
+
- Reconstruct/generate/redesign: read [references/design-modes.md](references/design-modes.md), then only the matching platform section from [references/platform-standards.md](references/platform-standards.md). Use cached evidence and `uidw workbench`; validate transfer fidelity, not product UX. For Android or Apple visual accuracy also read [references/native-rendering.md](references/native-rendering.md) and call MCP `ui_native_status` (or `uidw native status`).
|
|
28
|
+
- Explicit review/audit/critique: read [references/ui-reviewer.md](references/ui-reviewer.md) and [references/review-workflow.md](references/review-workflow.md). Review immutable Before only and work in bounded screen batches.
|
|
29
|
+
- Workbench chrome behavior: read [references/workbench-ui.md](references/workbench-ui.md).
|
|
30
|
+
- Ordinary UI source task with `uiMode.enabled=true`: read [references/ui-guidance-mode.md](references/ui-guidance-mode.md). Do not create a preview or review unless requested.
|
|
31
|
+
- Cache/install/agent integration question: read only [references/cache-protocol.md](references/cache-protocol.md) or [references/agent-integrations.md](references/agent-integrations.md), respectively.
|
|
32
|
+
|
|
33
|
+
## 3. Return sparse changes
|
|
34
|
+
|
|
35
|
+
Review/proposal agents write `ui-ir.patch.json`, never a replacement full IR:
|
|
36
|
+
|
|
37
|
+
```text
|
|
38
|
+
uidw patch validate <ui-ir.patch.json> --ir <ui-ir.json>
|
|
39
|
+
uidw patch apply <ui-ir.patch.json> --ir <ui-ir.json> --output <ui-ir.proposed.json>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Allowed operations are `upsert-findings`, `upsert-versions`, `merge-annotations`, and `record-verifications`. Baseline screens, nodes, tokens, themes, fixtures, and application source are immutable in this path. Create one restrained proposal by default; add a second only for a real UX tradeoff.
|
|
43
|
+
|
|
44
|
+
## Invariants
|
|
45
|
+
|
|
46
|
+
- Reconstruction, generation, redesign, Low/Medium/High, `workbench`, and `check` never trigger an automatic UI/UX audit. Only an explicit review request creates findings.
|
|
47
|
+
- Do not run the target app, emulator, simulator, build, or dev server unless separately requested. Headless checks may open only generated local HTML.
|
|
48
|
+
- Preserve discovered screens, routes, project components, platform idioms, tokens, themes, states, provenance, and unsupported gaps. Do not invent polish or browser-default replacements.
|
|
49
|
+
- HTML is a projection. Base IR is immutable Before; proposal versions are sparse overrides.
|
|
50
|
+
- Android/Apple source translation is structural-only until a source-matched native capture exists. Never call HTML a native render.
|
|
51
|
+
- Applying a proposal to real source requires a separate explicit `Apply to project` or direct-fix authorization, bounded source targets, incremental sync, and targeted verification. Never repeat the full AI review automatically.
|
|
52
|
+
- Keep cache and review artifacts outside source control unless the user explicitly requests otherwise.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Fidelity adapter API
|
|
2
|
+
|
|
3
|
+
Fidelity adapters are deterministic, read-only translators from repository source to UI IR. They do not run the application, a browser dev server, an emulator, or a build.
|
|
4
|
+
|
|
5
|
+
`SourceContext` contains the repository root, relative source path, text, detected platforms, and scanner role. An adapter exposes a stable `id`, `supports(context)`, and `translate(context) -> AdapterResult`. Results contain screens, nodes, tokens, themes, component mappings, and an explicit unsupported-expression list. Register additional adapters with `register_adapter`; the scanner consumes the registry rather than importing provider-specific agents.
|
|
6
|
+
|
|
7
|
+
Every reconstructed visual or behavioral property must have a same-path entry in `node.provenance`, for example `style.background` or `layout.padding`. Evidence records have a stable hash ID, source file and line, original expression, adapter ID, and one of `exact`, `high`, `approximate`, or `unsupported`. Unsupported syntax is reported rather than guessed.
|
|
8
|
+
|
|
9
|
+
The built-in adapters are:
|
|
10
|
+
|
|
11
|
+
- `web`: static HTML plus deterministic simple CSS selectors, CSS custom properties, inline styles, semantic controls, lists, images, and text;
|
|
12
|
+
- `react-jsx`, `vue`, `svelte`: source markup projected through the Web contract; dynamic expressions and unmapped project components remain unsupported;
|
|
13
|
+
- `compose`: Android `@Composable` screens, common Material/Compose primitives, resources, modifiers, and tokens;
|
|
14
|
+
- `android-xml`: Android layout/resources XML, common Views/Material controls, dimensions/colors, and `values-night` token overrides;
|
|
15
|
+
- `swiftui`: iOS/macOS `View` bodies, common containers and controls, Asset Catalog resources, localization, SF Symbol fallbacks, modifiers, and source tokens;
|
|
16
|
+
- `apple-interface-xml`: Storyboard/XIB controllers and common UIKit/AppKit views with assets, authored frames, and a conservative Auto Layout subset;
|
|
17
|
+
- `xaml`: WinUI/WPF pages, common controls, resources, and basic layout/style attributes without evaluating bindings or templates;
|
|
18
|
+
- `flutter`: common Material/Cupertino widget trees and basic dimensions without executing Dart.
|
|
19
|
+
|
|
20
|
+
Golden source/expected fixtures live under `fixtures/golden`. Changing adapter output requires an intentional fixture update and the fidelity test suite. `uidw fidelity capabilities` returns the same installed matrix in machine-readable JSON.
|
ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/agent-integrations.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Agent integrations
|
|
2
|
+
|
|
3
|
+
The repository follows the open Agent Skills layout: one `SKILL.md` plus `scripts/`, `references/`, and optional agent-specific metadata. The core workflow communicates through files and shell commands, not a proprietary API.
|
|
4
|
+
|
|
5
|
+
## Discovery locations
|
|
6
|
+
|
|
7
|
+
`~/.agents/skills/ui-design-workbench` is the preferred portable installation where supported. Native locations are also available:
|
|
8
|
+
|
|
9
|
+
| Agent | Native user location |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| Codex | `~/.codex/skills/ui-design-workbench` |
|
|
12
|
+
| Claude Code | `~/.claude/skills/ui-design-workbench` |
|
|
13
|
+
| Cursor | `~/.cursor/skills/ui-design-workbench` |
|
|
14
|
+
| Gemini CLI | `~/.gemini/skills/ui-design-workbench` |
|
|
15
|
+
| GitHub Copilot CLI | `~/.copilot/skills/ui-design-workbench` |
|
|
16
|
+
| OpenCode | `~/.config/opencode/skills/ui-design-workbench` |
|
|
17
|
+
|
|
18
|
+
Install the published CLI and then copy its packaged Agent Skill into any supported discovery location:
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
pipx install ui-design-workbench-cli
|
|
22
|
+
uidw install-skill codex
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Use `uidw install-skill all` for every supported location. Repeating the command refreshes UIDW-managed copies after a CLI upgrade and refuses to overwrite an unrelated existing skill. The packaged skill includes its references, schemas, and CLI fallback scripts, so it does not depend on a Git checkout.
|
|
26
|
+
|
|
27
|
+
For contributor checkouts, `./install.ps1 -Agent all` on Windows or `./install.sh all` on macOS/Linux still creates development links to the repository. Repository-local installation can use `.agents/skills/ui-design-workbench` when team policy prefers checked-out skills.
|
|
28
|
+
|
|
29
|
+
## Capability contract
|
|
30
|
+
|
|
31
|
+
An agent needs local file read/write access and Python 3.10+. Node.js plus Chromium/Edge is optional but required for headless interaction and geometry diagnostics. The target application, emulator, simulator, bridge server, and internet access are not required.
|
|
32
|
+
|
|
33
|
+
The portable handoff is `ui-agent-job.json`. It contains artifact directory, review scope, selected stable finding IDs, screen IDs, active/baseline versions, annotations, allowed output patterns, and `sourceChangeAllowed: false`. It also points to a bounded `ui-agent-context.json` and a sparse `ui-ir.patch.json`; the agent reads the bounded context and returns patch operations instead of loading or replacing the complete IR. Agents must modify only the declared review artifacts and return validation results.
|
|
34
|
+
|
|
35
|
+
For ordinary source tasks, `uidw init` can opt a project into lightweight UI guidance. The mode is off by default and appears in `ui-context.json` as `uiMode`. A compatible agent loads only the relevant platform guidance, preserves the project's components and tokens, and stays within the requested implementation scope. This is independent of the review handoff and must not create a workbench or claim a full audit unless requested.
|
|
36
|
+
|
|
37
|
+
Codex may opt into `render_preview.py --agent codex`, which enables the official local `codex://new` preparation path. Other agents use copied prompts or the JSON job. Provider-specific adapters are optional presentation conveniences and must not change the IR, cache, review semantics, permissions, or approval boundary.
|
|
38
|
+
|
|
39
|
+
The optional `uidw-mcp` stdio server is a transport facade over the same deterministic CLI core. It exposes compact project discovery, bounded screen/finding context, job preparation, sparse patch application, preview building, and fidelity explanation. MCP is an optional dependency; installing or running the regular CLI must not require it. Configure an agent to launch `uidw-mcp --repo <absolute-project-path>` as a local stdio command; it opens no port and keeps all project data on the local machine.
|
ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/cache-protocol.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Project context and cache protocol
|
|
2
|
+
|
|
3
|
+
The cache accelerates discovery; it is never authoritative application data and never replaces source evidence.
|
|
4
|
+
|
|
5
|
+
## Storage model
|
|
6
|
+
|
|
7
|
+
The default is a user-level, project-keyed operating-system cache:
|
|
8
|
+
|
|
9
|
+
- Windows: `%LOCALAPPDATA%\UI Design Workbench\Cache\projects\<project-key>`
|
|
10
|
+
- macOS: `~/Library/Caches/ui-design-workbench/projects/<project-key>`
|
|
11
|
+
- Linux: `$XDG_CACHE_HOME/ui-design-workbench/projects/<project-key>` or `~/.cache/ui-design-workbench/projects/<project-key>`
|
|
12
|
+
|
|
13
|
+
Set `UIDW_CACHE_HOME` for an isolated runner. The per-project `config.json` lives in this user cache by default. `init --project-cache` is an explicit opt-in that instead creates `.ui-design-workbench/config.json` and an ignore-all `.gitignore` inside the repository. Commit only a deliberately shared config or exported semantic UI map; never commit fingerprints, per-file records, timestamps, diagnostics, screenshots, or agent-local paths.
|
|
14
|
+
|
|
15
|
+
The project key is derived from the normalized absolute repository path. The installed skill contains only reusable code and reference data; it must not contain project caches.
|
|
16
|
+
|
|
17
|
+
## State files
|
|
18
|
+
|
|
19
|
+
- `cache-state.json`: scanner/config versions, candidate manifest, content fingerprints, and reusable per-file analysis.
|
|
20
|
+
- `ui-scan.json`: aggregated UI inventory from current per-file records.
|
|
21
|
+
- `ui-ir.json`: synchronized projection combining the generated source index with preserved authored detail; still not a finished review deliverable.
|
|
22
|
+
- `design-model.json`: durable authored screen/node/scenario detail preserved across source-index refreshes.
|
|
23
|
+
- `review-state.json`: findings, annotations, decisions, and proposal metadata preserved separately from discovery.
|
|
24
|
+
- `ui-context.json`: bounded model-facing summary with changed files and priority reads.
|
|
25
|
+
- `ui-context-<screen>.json`: one screen subtree and its directly referenced sources.
|
|
26
|
+
- `sync-report.json`: last invalidation reason, changed files, and affected screen IDs.
|
|
27
|
+
- `config.json`: cache preferences, the opt-in `uiMode.enabled` flag, and derived `mockData.mode`. Mock data is always enabled and follows detail level: `minimal`, `representative`, or `exhaustive`; it is not a separate preference.
|
|
28
|
+
|
|
29
|
+
## Commands
|
|
30
|
+
|
|
31
|
+
```text
|
|
32
|
+
uidw --repo <repo> init
|
|
33
|
+
uidw --repo <repo> status --json
|
|
34
|
+
uidw --repo <repo> sync
|
|
35
|
+
uidw --repo <repo> context --screen <screen-id> --json
|
|
36
|
+
uidw --repo <repo> context --screen <screen-id> --budget 4000 --format markdown
|
|
37
|
+
uidw --repo <repo> map --output <docs-or-artifact-dir>/ui-graph.json
|
|
38
|
+
uidw --repo <repo> diff
|
|
39
|
+
uidw --repo <repo> workbench --output-dir <artifact-dir>
|
|
40
|
+
uidw --repo <repo> check --ir <artifact-dir>/ui-ir.json --level full
|
|
41
|
+
uidw --repo <repo> doctor --json
|
|
42
|
+
uidw --repo <repo> ui-mode --enable
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`init`, `sync`, `context`, `map`, and cache-backed workbench/review entry points share one internal `ensure_initialized` bootstrap. The first ordinary `context` or `review` request creates the default user-cache configuration, UI index, graph, and starter IR automatically; manual `init` is not required. Each result exposes `initialization.status` as `created`, `updated`, `reused`, or `stale`. Later calls reuse the existing cache and do not analyze unchanged source files. Use `sync --force` after scanner/schema upgrades or known discovery errors. Use `--verify-content` when timestamps may be unreliable, such as restored archives or unusual network filesystems.
|
|
46
|
+
|
|
47
|
+
## Invalidation
|
|
48
|
+
|
|
49
|
+
Candidate files use size, nanosecond mtime, and SHA-256. Unchanged metadata reuses the existing digest; changed metadata is rehashed. The manifest watches broad source candidates so that newly added UI files can be discovered, but `changedUiFiles` contains only records classified as UI or paths already referenced by the UI inventory. Non-UI source changes refresh manifest metadata without invalidating the UI cache. Only added or content-modified UI files are analyzed again. Removed UI files drop their cached record. A theme or navigation change invalidates all screens; a screen/component change invalidates mapped dependents; scanner/config/cache-version changes force full reconstruction.
|
|
50
|
+
|
|
51
|
+
Metadata-only changes update the manifest without rescanning. `--verify-content` rehashes every candidate when correctness is more important than I/O cost.
|
|
52
|
+
|
|
53
|
+
Every JSON write uses a same-directory temporary file plus atomic replacement. A short-lived project state lock serializes concurrent writers. Configuration schema upgrades keep `config.v<old>.backup.json`; disposable scanner/cache versions rebuild automatically. Source changes never discard authored scenarios, proposals, findings, or annotations: bindings for impacted screens and nodes are retained with `sourceState: stale` until an agent reconciles them.
|
|
54
|
+
|
|
55
|
+
Do not run a watcher, bridge server, emulator, or target application. Lazy command-boundary synchronization is deterministic, portable, and inexpensive for agent workflows.
|
|
56
|
+
|
|
57
|
+
Interactive `init` asks only for detail level and does not recommend or preselect a value. Automatic non-interactive initialization writes only the neutral technical config and never guesses a detail level: compact context reports `configuration.setupRequired` and one concise `questionsForUser` item until the user answers it. Use `config setup`, `config show`, or `config set detail <low|medium|high>` without rescanning unchanged UI files. `help config` describes the levels and `about` explains the tool boundary.
|
|
58
|
+
|
|
59
|
+
UI guidance is not asked during setup and remains off unless explicitly enabled. `init --ui-mode` and `init --no-ui-mode` make automation deterministic. Later use `ui-mode --enable`, `ui-mode --disable`, or `config set ui-mode on|off`. Enabling guidance changes agent behavior only for UI-related tasks and does not imply review, redesign, preview generation, or source-edit permission.
|
|
60
|
+
|
|
61
|
+
Mock data follows detail level automatically: `low` uses `minimal`, `medium` uses `representative`, and `high` uses `exhaustive`. This derived change refreshes model context but never causes another repository source scan by itself. Preserve existing `scenarioFixtures` and `screen.scenarios` for unaffected screen IDs during incremental sync; regenerate only fixtures whose source dependencies changed.
|