ui-design-workbench-cli 0.6.6__tar.gz → 0.6.7__tar.gz

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.
Files changed (63) hide show
  1. {ui_design_workbench_cli-0.6.6 → ui_design_workbench_cli-0.6.7}/PKG-INFO +2 -2
  2. {ui_design_workbench_cli-0.6.6 → ui_design_workbench_cli-0.6.7}/README.md +1 -1
  3. {ui_design_workbench_cli-0.6.6 → ui_design_workbench_cli-0.6.7}/pyproject.toml +1 -1
  4. {ui_design_workbench_cli-0.6.6 → ui_design_workbench_cli-0.6.7}/scripts/fidelity_platform_adapters.py +27 -3
  5. {ui_design_workbench_cli-0.6.6 → ui_design_workbench_cli-0.6.7}/scripts/render_preview.py +9 -5
  6. {ui_design_workbench_cli-0.6.6 → ui_design_workbench_cli-0.6.7}/scripts/scan_ui.py +71 -1
  7. {ui_design_workbench_cli-0.6.6 → ui_design_workbench_cli-0.6.7}/scripts/ui_design_workbench_cli.egg-info/PKG-INFO +2 -2
  8. {ui_design_workbench_cli-0.6.6 → ui_design_workbench_cli-0.6.7}/scripts/uidw.py +21 -1
  9. {ui_design_workbench_cli-0.6.6 → ui_design_workbench_cli-0.6.7}/SKILL.md +0 -0
  10. {ui_design_workbench_cli-0.6.6 → ui_design_workbench_cli-0.6.7}/references/adapter-api.md +0 -0
  11. {ui_design_workbench_cli-0.6.6 → ui_design_workbench_cli-0.6.7}/references/agent-integrations.md +0 -0
  12. {ui_design_workbench_cli-0.6.6 → ui_design_workbench_cli-0.6.7}/references/cache-protocol.md +0 -0
  13. {ui_design_workbench_cli-0.6.6 → ui_design_workbench_cli-0.6.7}/references/component-catalog.json +0 -0
  14. {ui_design_workbench_cli-0.6.6 → ui_design_workbench_cli-0.6.7}/references/component-catalog.md +0 -0
  15. {ui_design_workbench_cli-0.6.6 → ui_design_workbench_cli-0.6.7}/references/component-inventory.json +0 -0
  16. {ui_design_workbench_cli-0.6.6 → ui_design_workbench_cli-0.6.7}/references/design-modes.md +0 -0
  17. {ui_design_workbench_cli-0.6.6 → ui_design_workbench_cli-0.6.7}/references/fidelity.md +0 -0
  18. {ui_design_workbench_cli-0.6.6 → ui_design_workbench_cli-0.6.7}/references/ir-schema.md +0 -0
  19. {ui_design_workbench_cli-0.6.6 → ui_design_workbench_cli-0.6.7}/references/native-rendering.md +0 -0
  20. {ui_design_workbench_cli-0.6.6 → ui_design_workbench_cli-0.6.7}/references/platform-profiles.json +0 -0
  21. {ui_design_workbench_cli-0.6.6 → ui_design_workbench_cli-0.6.7}/references/platform-standards.md +0 -0
  22. {ui_design_workbench_cli-0.6.6 → ui_design_workbench_cli-0.6.7}/references/platforms.md +0 -0
  23. {ui_design_workbench_cli-0.6.6 → ui_design_workbench_cli-0.6.7}/references/quality-automation.md +0 -0
  24. {ui_design_workbench_cli-0.6.6 → ui_design_workbench_cli-0.6.7}/references/review-workflow.md +0 -0
  25. {ui_design_workbench_cli-0.6.6 → ui_design_workbench_cli-0.6.7}/references/ui-guidance-mode.md +0 -0
  26. {ui_design_workbench_cli-0.6.6 → ui_design_workbench_cli-0.6.7}/references/ui-policy-schema.md +0 -0
  27. {ui_design_workbench_cli-0.6.6 → ui_design_workbench_cli-0.6.7}/references/ui-reviewer.md +0 -0
  28. {ui_design_workbench_cli-0.6.6 → ui_design_workbench_cli-0.6.7}/references/validation.md +0 -0
  29. {ui_design_workbench_cli-0.6.6 → ui_design_workbench_cli-0.6.7}/references/workbench-ui.md +0 -0
  30. {ui_design_workbench_cli-0.6.6 → ui_design_workbench_cli-0.6.7}/schemas/native-render-state.schema.json +0 -0
  31. {ui_design_workbench_cli-0.6.6 → ui_design_workbench_cli-0.6.7}/schemas/ui-agent-job.schema.json +0 -0
  32. {ui_design_workbench_cli-0.6.6 → ui_design_workbench_cli-0.6.7}/schemas/ui-graph.schema.json +0 -0
  33. {ui_design_workbench_cli-0.6.6 → ui_design_workbench_cli-0.6.7}/schemas/ui-ir.patch.schema.json +0 -0
  34. {ui_design_workbench_cli-0.6.6 → ui_design_workbench_cli-0.6.7}/schemas/ui-ir.schema.json +0 -0
  35. {ui_design_workbench_cli-0.6.6 → ui_design_workbench_cli-0.6.7}/schemas/uidw-config.schema.json +0 -0
  36. {ui_design_workbench_cli-0.6.6 → ui_design_workbench_cli-0.6.7}/scripts/android_resource_resolver.py +0 -0
  37. {ui_design_workbench_cli-0.6.6 → ui_design_workbench_cli-0.6.7}/scripts/android_xml_support.py +0 -0
  38. {ui_design_workbench_cli-0.6.6 → ui_design_workbench_cli-0.6.7}/scripts/apple_resource_resolver.py +0 -0
  39. {ui_design_workbench_cli-0.6.6 → ui_design_workbench_cli-0.6.7}/scripts/coverage_report.py +0 -0
  40. {ui_design_workbench_cli-0.6.6 → ui_design_workbench_cli-0.6.7}/scripts/fidelity_adapter_api.py +0 -0
  41. {ui_design_workbench_cli-0.6.6 → ui_design_workbench_cli-0.6.7}/scripts/fidelity_adapters.py +0 -0
  42. {ui_design_workbench_cli-0.6.6 → ui_design_workbench_cli-0.6.7}/scripts/fidelity_core.py +0 -0
  43. {ui_design_workbench_cli-0.6.6 → ui_design_workbench_cli-0.6.7}/scripts/generate_interaction_matrix.py +0 -0
  44. {ui_design_workbench_cli-0.6.6 → ui_design_workbench_cli-0.6.7}/scripts/ir_contracts.py +0 -0
  45. {ui_design_workbench_cli-0.6.6 → ui_design_workbench_cli-0.6.7}/scripts/layout_model.py +0 -0
  46. {ui_design_workbench_cli-0.6.6 → ui_design_workbench_cli-0.6.7}/scripts/merge_review_state.py +0 -0
  47. {ui_design_workbench_cli-0.6.6 → ui_design_workbench_cli-0.6.7}/scripts/native_render_android.py +0 -0
  48. {ui_design_workbench_cli-0.6.6 → ui_design_workbench_cli-0.6.7}/scripts/native_render_apple.py +0 -0
  49. {ui_design_workbench_cli-0.6.6 → ui_design_workbench_cli-0.6.7}/scripts/native_render_contracts.py +0 -0
  50. {ui_design_workbench_cli-0.6.6 → ui_design_workbench_cli-0.6.7}/scripts/native_render_registry.py +0 -0
  51. {ui_design_workbench_cli-0.6.6 → ui_design_workbench_cli-0.6.7}/scripts/platform_component_catalog.py +0 -0
  52. {ui_design_workbench_cli-0.6.6 → ui_design_workbench_cli-0.6.7}/scripts/quality_common.py +0 -0
  53. {ui_design_workbench_cli-0.6.6 → ui_design_workbench_cli-0.6.7}/scripts/scoped_context.py +0 -0
  54. {ui_design_workbench_cli-0.6.6 → ui_design_workbench_cli-0.6.7}/scripts/smoke_preview.js +0 -0
  55. {ui_design_workbench_cli-0.6.6 → ui_design_workbench_cli-0.6.7}/scripts/ui_design_workbench_cli.egg-info/SOURCES.txt +0 -0
  56. {ui_design_workbench_cli-0.6.6 → ui_design_workbench_cli-0.6.7}/scripts/ui_design_workbench_cli.egg-info/dependency_links.txt +0 -0
  57. {ui_design_workbench_cli-0.6.6 → ui_design_workbench_cli-0.6.7}/scripts/ui_design_workbench_cli.egg-info/entry_points.txt +0 -0
  58. {ui_design_workbench_cli-0.6.6 → ui_design_workbench_cli-0.6.7}/scripts/ui_design_workbench_cli.egg-info/requires.txt +0 -0
  59. {ui_design_workbench_cli-0.6.6 → ui_design_workbench_cli-0.6.7}/scripts/ui_design_workbench_cli.egg-info/top_level.txt +0 -0
  60. {ui_design_workbench_cli-0.6.6 → ui_design_workbench_cli-0.6.7}/scripts/uidw_mcp.py +0 -0
  61. {ui_design_workbench_cli-0.6.6 → ui_design_workbench_cli-0.6.7}/scripts/validate_platform_profiles.py +0 -0
  62. {ui_design_workbench_cli-0.6.6 → ui_design_workbench_cli-0.6.7}/scripts/visual_regression.py +0 -0
  63. {ui_design_workbench_cli-0.6.6 → ui_design_workbench_cli-0.6.7}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ui-design-workbench-cli
3
- Version: 0.6.6
3
+ Version: 0.6.7
4
4
  Summary: Repository UI indexing, graph, preview, and deterministic review CLI
5
5
  Author: Elgreed
6
6
  Project-URL: Homepage, https://github.com/Elgreed/ui-design-workbench
@@ -131,4 +131,4 @@ A Windows `.exe` is not published yet. PyPI + `pipx` is the primary cross-platfo
131
131
  - [IR schema](references/ir-schema.md)
132
132
  - [Review workflow](references/review-workflow.md)
133
133
 
134
- Current CLI version: `0.6.6`.
134
+ Current CLI version: `0.6.7`.
@@ -112,4 +112,4 @@ A Windows `.exe` is not published yet. PyPI + `pipx` is the primary cross-platfo
112
112
  - [IR schema](references/ir-schema.md)
113
113
  - [Review workflow](references/review-workflow.md)
114
114
 
115
- Current CLI version: `0.6.6`.
115
+ Current CLI version: `0.6.7`.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ui-design-workbench-cli"
7
- version = "0.6.6"
7
+ version = "0.6.7"
8
8
  description = "Repository UI indexing, graph, preview, and deterministic review CLI"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -272,7 +272,15 @@ class AndroidXmlAdapter:
272
272
  for attr, (path, group) in mapping.items():
273
273
  if attr in attrs:
274
274
  resolved = catalog.resolve(attrs[attr], group, normalized_style)
275
- value = resolved.value if resolved.resolved else _normalize_ref(attrs[attr], "colors" if group == "color" else group)
275
+ value = _normalize_ref(str(resolved.value), "colors" if group == "color" else group) if resolved.resolved else attrs[attr]
276
+ if not resolved.resolved and str(attrs[attr]).startswith(("@", "?")):
277
+ result.unsupported.append({
278
+ "adapter": self.id,
279
+ "file": active_context.source,
280
+ "line": line,
281
+ "expression": f'{attr}="{attrs[attr]}"',
282
+ "reason": "unresolved-android-resource",
283
+ })
276
284
  source = resolved.source or (style_entry.source if attr not in explicit_attrs and style_entry else active_context.source)
277
285
  source_line = resolved.line if resolved.source else style_entry.line if attr not in explicit_attrs and style_entry else line
278
286
  group_name, key = path.split(".", 1)
@@ -319,7 +327,9 @@ class AndroidXmlAdapter:
319
327
  result.unsupported.append({"adapter": self.id, "file": active_context.source, "line": line, "expression": ", ".join(sorted(unsupported_constraints)), "reason": "unsupported-constraint-equation"})
320
328
  if attrs.get("text"):
321
329
  resolved = catalog.resolve(attrs["text"], "string", normalized_style)
322
- node["text"] = resolved.value if resolved.resolved else _normalize_ref(attrs["text"], "strings")
330
+ node["text"] = resolved.value if resolved.resolved else attrs["text"]
331
+ if not resolved.resolved and str(attrs["text"]).startswith(("@", "?")):
332
+ result.unsupported.append({"adapter": self.id, "file": active_context.source, "line": line, "expression": f'text="{attrs["text"]}"', "reason": "unresolved-android-resource"})
323
333
  node["provenance"]["text"] = property_evidence(resolved.source or active_context.source, resolved.line if resolved.source else line, f'text="{attrs["text"]}"', self.id, resolved.confidence if resolved.source else "exact")
324
334
  if attrs.get("hint"):
325
335
  resolved = catalog.resolve(attrs["hint"], "string", normalized_style)
@@ -347,6 +357,14 @@ class AndroidXmlAdapter:
347
357
  node["asset"] = drawable.asset or asset
348
358
  node["style"].update(drawable.style)
349
359
  node["provenance"]["asset"] = property_evidence(drawable.source or active_context.source, drawable.line if drawable.source else line, str(asset), self.id, drawable.confidence)
360
+ for key in drawable.style:
361
+ node["provenance"][f"style.{key}"] = property_evidence(
362
+ drawable.source or active_context.source,
363
+ drawable.line if drawable.source else line,
364
+ str(asset),
365
+ self.id,
366
+ drawable.confidence,
367
+ )
350
368
  if not drawable.asset:
351
369
  result.unsupported.append({"adapter": self.id, "file": active_context.source, "line": line, "expression": asset, "reason": "unsupported-android-drawable"})
352
370
  scale_types = {"centerCrop": "cover", "fitCenter": "contain", "centerInside": "contain", "fitXY": "fill", "center": "none"}
@@ -361,7 +379,13 @@ class AndroidXmlAdapter:
361
379
  "type": "icon", "component": "ImageButton.drawable", "asset": node["asset"], "alt": node.get("alt", ""),
362
380
  "layout": {"width": "fill", "height": "fill"}, "style": {"objectFit": node.get("style", {}).get("objectFit", "contain")}, "children": [],
363
381
  "source": {"file": active_context.source, "line": line}, "confidence": node.get("confidence", "high"), "standardRef": node.get("standardRef"),
364
- "provenance": {"asset": node["provenance"].get("asset", property_evidence(active_context.source, line, str(node["asset"]), self.id, "exact"))},
382
+ "provenance": {
383
+ "component": property_evidence(active_context.source, line, "ImageButton drawable", self.id, "high"),
384
+ "asset": node["provenance"].get("asset", property_evidence(active_context.source, line, str(node["asset"]), self.id, "exact")),
385
+ "layout.width": property_evidence(active_context.source, line, "ImageButton drawable bounds", self.id, "high"),
386
+ "layout.height": property_evidence(active_context.source, line, "ImageButton drawable bounds", self.id, "high"),
387
+ "style.objectFit": node["provenance"].get("style.objectFit", property_evidence(active_context.source, line, "ImageButton drawable scale", self.id, "high")),
388
+ },
365
389
  }
366
390
  node["children"].insert(0, icon_id)
367
391
  if node.get("layout", {}).get("direction") == "overlay":
@@ -676,11 +676,15 @@ def fidelity_audit(ir: dict[str, Any]) -> dict[str, Any]:
676
676
  excluded_routes, invalid_route_exclusions = exclusion_keys(fidelity.get("excludedRoutes", []), "route")
677
677
  discovered_screens = ir.get("discoveredScreens", [])
678
678
  discovered_routes = ir.get("discoveredRoutes", [])
679
- translated_screen_keys = {
680
- str(screen.get("discoveredKey") or f'{screen.get("source", {}).get("file", "")}#{screen.get("source", {}).get("symbol", screen.get("name", ""))}')
681
- for screen in screens
682
- if screen.get("translationStatus") != "inventory"
683
- }
679
+ translated_screen_keys: set[str] = set()
680
+ for screen in screens:
681
+ if screen.get("translationStatus") == "inventory":
682
+ continue
683
+ translated_screen_keys.add(str(
684
+ screen.get("discoveredKey")
685
+ or f'{screen.get("source", {}).get("file", "")}#{screen.get("source", {}).get("symbol", screen.get("name", ""))}'
686
+ ))
687
+ translated_screen_keys.update(str(key) for key in screen.get("discoveredKeys", []) if key)
684
688
  expected_screen_keys = {f'{screen.get("file", "")}#{screen.get("name", "")}' for screen in discovered_screens}
685
689
  missing_screen_keys = sorted(expected_screen_keys - translated_screen_keys - excluded_screens)
686
690
  translated_routes = {
@@ -1022,6 +1022,26 @@ def evidence_for_screen(screen: dict[str, Any], discovered: list[dict[str, Any]]
1022
1022
  source_file = str(screen.get("source", {}).get("file") or "")
1023
1023
  fragment = str(screen.get("fragment") or "")
1024
1024
  symbol = str(screen.get("source", {}).get("symbol") or screen.get("name") or "")
1025
+ discovered_key = str(screen.get("discoveredKey") or "")
1026
+ navigation_file = str(screen.get("navigationSource", {}).get("file") or "")
1027
+ if discovered_key:
1028
+ matched = next((
1029
+ candidate for candidate in discovered
1030
+ if f'{candidate.get("file", "")}#{candidate.get("name", "")}' == discovered_key
1031
+ ), None)
1032
+ if matched:
1033
+ return matched
1034
+ if fragment:
1035
+ fragment_matches = [
1036
+ candidate for candidate in discovered
1037
+ if str(candidate.get("fragment") or "") == fragment
1038
+ ]
1039
+ if navigation_file:
1040
+ matched = next((candidate for candidate in fragment_matches if str(candidate.get("file") or "") == navigation_file), None)
1041
+ if matched:
1042
+ return matched
1043
+ if len(fragment_matches) == 1:
1044
+ return fragment_matches[0]
1025
1045
  return next((
1026
1046
  candidate for candidate in discovered
1027
1047
  if str(candidate.get("file") or candidate.get("source", {}).get("file") or "") == source_file
@@ -1032,6 +1052,39 @@ def evidence_for_screen(screen: dict[str, Any], discovered: list[dict[str, Any]]
1032
1052
  ), {})
1033
1053
 
1034
1054
 
1055
+ def consolidate_translated_android_variants(translated_screens: list[dict[str, Any]]) -> None:
1056
+ """Keep one canonical screen per Android layout while retaining qualifier evidence."""
1057
+ grouped: dict[str, list[dict[str, Any]]] = {}
1058
+ for screen in translated_screens:
1059
+ if screen.get("platform") != "android" or not screen.get("androidLayout"):
1060
+ continue
1061
+ key = str(screen.get("id") or screen.get("androidLayout"))
1062
+ grouped.setdefault(key, []).append(screen)
1063
+
1064
+ consolidated: list[dict[str, Any]] = []
1065
+ emitted: set[str] = set()
1066
+ for screen in translated_screens:
1067
+ if screen.get("platform") != "android" or not screen.get("androidLayout"):
1068
+ consolidated.append(screen)
1069
+ continue
1070
+ key = str(screen.get("id") or screen.get("androidLayout"))
1071
+ if key in emitted:
1072
+ continue
1073
+ emitted.add(key)
1074
+ variants = grouped[key]
1075
+ canonical = next((item for item in variants if not item.get("resourceQualifier")), variants[-1])
1076
+ if len(variants) > 1:
1077
+ canonical["resourceVariants"] = [
1078
+ {
1079
+ "qualifier": str(item.get("resourceQualifier") or "default"),
1080
+ "source": dict(item.get("source", {})),
1081
+ }
1082
+ for item in variants
1083
+ ]
1084
+ consolidated.append(canonical)
1085
+ translated_screens[:] = consolidated
1086
+
1087
+
1035
1088
  def reconcile_translated_android_screens(
1036
1089
  translated_screens: list[dict[str, Any]],
1037
1090
  discovered: list[dict[str, Any]],
@@ -1313,6 +1366,7 @@ def starter_ir(scan_result: dict[str, Any]) -> dict[str, Any]:
1313
1366
  for context in adapter_contexts
1314
1367
  ]
1315
1368
  translated = translate_sources(adapter_contexts)
1369
+ consolidate_translated_android_variants(translated.screens)
1316
1370
  resolve_translated_android_includes(translated)
1317
1371
  token_resolver = TokenResolver(translated.tokens)
1318
1372
  for node in translated.nodes.values():
@@ -1325,6 +1379,16 @@ def starter_ir(scan_result: dict[str, Any]) -> dict[str, Any]:
1325
1379
  raw_scan_screens = scan_result.get("screens", [])
1326
1380
  reconcile_translated_android_screens(translated.screens, raw_scan_screens)
1327
1381
  reconcile_translated_screens(translated.screens, raw_scan_screens)
1382
+ for screen in translated.screens:
1383
+ variant_keys = []
1384
+ for variant in screen.get("resourceVariants", []):
1385
+ variant_file = str(variant.get("source", {}).get("file") or "")
1386
+ candidate = next((item for item in raw_scan_screens if str(item.get("file") or "") == variant_file), None)
1387
+ if candidate:
1388
+ variant_keys.append(f'{candidate.get("file", "")}#{candidate.get("name", "")}')
1389
+ keys = [str(screen.get("discoveredKey") or ""), *variant_keys]
1390
+ if any(keys):
1391
+ screen["discoveredKeys"] = list(dict.fromkeys(key for key in keys if key))
1328
1392
  if scan_result.get("primaryPlatforms") == ["flutter"]:
1329
1393
  discovered_flutter = {str(item.get("name") or "") for item in raw_scan_screens}
1330
1394
  translated.screens[:] = [
@@ -1447,9 +1511,15 @@ def starter_ir(scan_result: dict[str, Any]) -> dict[str, Any]:
1447
1511
  for item in screens
1448
1512
  }
1449
1513
  represented_discovered = {str(item.get("discoveredKey") or "") for item in screens if item.get("discoveredKey")}
1514
+ represented_variant_files = {
1515
+ str(variant.get("source", {}).get("file") or "")
1516
+ for item in screens
1517
+ for variant in item.get("resourceVariants", [])
1518
+ if isinstance(variant, dict)
1519
+ }
1450
1520
  for inventory in inventory_screens:
1451
1521
  key = (str(inventory.get("source", {}).get("file") or ""), str(inventory.get("fragment") or ""))
1452
- if key in represented or str(inventory.get("discoveredKey") or "") in represented_discovered:
1522
+ if key[0] in represented_variant_files or key in represented or str(inventory.get("discoveredKey") or "") in represented_discovered:
1453
1523
  continue
1454
1524
  screens.append(inventory)
1455
1525
  stack = [str(inventory.get("root") or "")]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ui-design-workbench-cli
3
- Version: 0.6.6
3
+ Version: 0.6.7
4
4
  Summary: Repository UI indexing, graph, preview, and deterministic review CLI
5
5
  Author: Elgreed
6
6
  Project-URL: Homepage, https://github.com/Elgreed/ui-design-workbench
@@ -131,4 +131,4 @@ A Windows `.exe` is not published yet. PyPI + `pipx` is the primary cross-platfo
131
131
  - [IR schema](references/ir-schema.md)
132
132
  - [Review workflow](references/review-workflow.md)
133
133
 
134
- Current CLI version: `0.6.6`.
134
+ Current CLI version: `0.6.7`.
@@ -53,7 +53,7 @@ from scan_ui import (
53
53
 
54
54
 
55
55
  CACHE_VERSION = 8
56
- CLI_VERSION = "0.6.6"
56
+ CLI_VERSION = "0.6.7"
57
57
  CONFIG_VERSION = 5
58
58
  STATE_DIR_NAME = ".ui-design-workbench"
59
59
  CONFIG_NAME = "config.json"
@@ -1036,6 +1036,24 @@ def prune_missing_token_refs(value: Any, tokens: dict[str, Any]) -> Any:
1036
1036
  return copy.deepcopy(value)
1037
1037
 
1038
1038
 
1039
+ def prune_orphan_provenance(node: dict[str, Any]) -> dict[str, Any]:
1040
+ """Remove evidence entries for authored properties pruned during cache migration."""
1041
+ provenance = node.get("provenance")
1042
+ if not isinstance(provenance, dict):
1043
+ return node
1044
+
1045
+ def path_exists(path: str) -> bool:
1046
+ current: Any = node
1047
+ for part in path.split("."):
1048
+ if not isinstance(current, dict) or part not in current:
1049
+ return False
1050
+ current = current[part]
1051
+ return True
1052
+
1053
+ node["provenance"] = {path: evidence for path, evidence in provenance.items() if path_exists(str(path))}
1054
+ return node
1055
+
1056
+
1039
1057
  def merge_authored_state(
1040
1058
  generated: dict[str, Any],
1041
1059
  design_model: dict[str, Any],
@@ -1074,6 +1092,7 @@ def merge_authored_state(
1074
1092
  cleaned_authored = prune_missing_token_refs(authored, merged_tokens)
1075
1093
  if not isinstance(cleaned_authored, dict):
1076
1094
  continue
1095
+ prune_orphan_provenance(cleaned_authored)
1077
1096
  existing = result.setdefault("nodes", {}).get(node_id, {})
1078
1097
  merged = copy.deepcopy(existing)
1079
1098
  for key, value in cleaned_authored.items():
@@ -1085,6 +1104,7 @@ def merge_authored_state(
1085
1104
  merged["sourceState"] = "stale"
1086
1105
  else:
1087
1106
  merged.pop("sourceState", None)
1107
+ prune_orphan_provenance(merged)
1088
1108
  result["nodes"][node_id] = merged
1089
1109
 
1090
1110
  fixtures = design_model.get("scenarioFixtures", {})