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
uidw_mcp.py
ADDED
|
@@ -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())
|