Skip to main content

#!/usr/bin/env python3 """ A2UI to Protocode Transformer

Transforms A2UI (Agent-to-UI) semantic trees into Protocode intermediate representation with design token bindings. Part of ADR-091 Unified Design System.

The transformer:

  1. Converts A2UI semantic nodes to Protocode nodes
  2. Applies component token mappings from the component library
  3. Resolves token references from design-system.json
  4. Outputs framework-agnostic Protocode ready for code generation

Usage: python a2ui_transformer.py input.a2ui.json -o output.protocode.json python a2ui_transformer.py input.a2ui.json --design-system design-system.json

Author: CODITECT Architecture Team Version: 1.0.0 ADR: ADR-091 """

import json import sys from pathlib import Path from typing import Any, Dict, List, Optional, Union from dataclasses import dataclass, field, asdict from enum import Enum import uuid

class NodeType(str, Enum): ELEMENT = "element" COMPONENT = "component" FRAGMENT = "fragment" TEXT = "text" SLOT = "slot"

class AtomicLevel(str, Enum): ATOM = "atom" MOLECULE = "molecule" ORGANISM = "organism" TEMPLATE = "template"

@dataclass class DataBinding: """Represents a dynamic data binding.""" source: str # "props", "state", "context", "computed" path: str transform: Optional[str] = None

@dataclass class ProtocodeEvent: """Event handler definition.""" name: str handler: str payload: Optional[Dict[str, Any]] = None

@dataclass class ProtocodeCondition: """Conditional rendering definition.""" type: str # "if", "show", "unless" expression: str

@dataclass class ProtocodeLoop: """List rendering definition.""" items: str item_name: str key_path: str

@dataclass class AccessibilitySpec: """Accessibility metadata.""" role: Optional[str] = None label: Optional[str] = None described_by: Optional[str] = None live: Optional[str] = None required: Optional[bool] = None

@dataclass class ResponsiveOverride: """Responsive style overrides.""" breakpoint: str styles: Dict[str, str]

@dataclass class ProtocodeStyles: """Style definitions with token bindings.""" token_bound: Dict[str, str] = field(default_factory=dict) resolved: Optional[Dict[str, str]] = None responsive: List[ResponsiveOverride] = field(default_factory=list)

@dataclass class ProtocodeProps: """Props with static values, dynamic bindings, and token references.""" static: Dict[str, Any] = field(default_factory=dict) dynamic: Dict[str, DataBinding] = field(default_factory=dict) tokens: Dict[str, str] = field(default_factory=dict)

@dataclass class ProtocodeMetadata: """Node metadata.""" source: str # Original A2UI node ID accessibility: Optional[AccessibilitySpec] = None responsive: Optional[Dict[str, Any]] = None animation: Optional[Dict[str, Any]] = None

@dataclass class ProtocodeNode: """ Protocode node representing a UI element or component.

This is the framework-agnostic intermediate representation
that can be transformed into React, Vue, Svelte, etc.
"""
id: str
node_type: NodeType
component: Optional[str] = None
atomic_level: Optional[AtomicLevel] = None
children: List['ProtocodeNode'] = field(default_factory=list)
text: Optional[str] = None
slots: Dict[str, List['ProtocodeNode']] = field(default_factory=dict)
props: ProtocodeProps = field(default_factory=ProtocodeProps)
styles: ProtocodeStyles = field(default_factory=ProtocodeStyles)
events: List[ProtocodeEvent] = field(default_factory=list)
conditions: Optional[ProtocodeCondition] = None
loop: Optional[ProtocodeLoop] = None
metadata: Optional[ProtocodeMetadata] = None

def to_dict(self) -> Dict[str, Any]:
"""Convert to JSON-serializable dict."""
result = {
"id": self.id,
"nodeType": self.node_type.value,
}

if self.component:
result["component"] = self.component
if self.atomic_level:
result["atomicLevel"] = self.atomic_level.value
if self.children:
result["children"] = [c.to_dict() for c in self.children]
if self.text:
result["text"] = self.text
if self.slots:
result["slots"] = {
k: [n.to_dict() for n in v]
for k, v in self.slots.items()
}

# Props
props_dict = {}
if self.props.static:
props_dict["static"] = self.props.static
if self.props.dynamic:
props_dict["dynamic"] = {
k: asdict(v) for k, v in self.props.dynamic.items()
}
if self.props.tokens:
props_dict["tokens"] = self.props.tokens
if props_dict:
result["props"] = props_dict

# Styles
styles_dict = {}
if self.styles.token_bound:
styles_dict["tokenBound"] = self.styles.token_bound
if self.styles.resolved:
styles_dict["resolved"] = self.styles.resolved
if self.styles.responsive:
styles_dict["responsive"] = [
{"breakpoint": r.breakpoint, "styles": r.styles}
for r in self.styles.responsive
]
if styles_dict:
result["styles"] = styles_dict

# Events
if self.events:
result["events"] = [asdict(e) for e in self.events]

# Conditions and loops
if self.conditions:
result["conditions"] = asdict(self.conditions)
if self.loop:
result["loop"] = asdict(self.loop)

# Metadata
if self.metadata:
metadata_dict = {"source": self.metadata.source}
if self.metadata.accessibility:
metadata_dict["accessibility"] = asdict(self.metadata.accessibility)
if self.metadata.responsive:
metadata_dict["responsive"] = self.metadata.responsive
if self.metadata.animation:
metadata_dict["animation"] = self.metadata.animation
result["metadata"] = metadata_dict

return result

Component token mappings based on DESIGN-SYSTEM-COMPONENT-MAPPING.md

COMPONENT_TOKEN_MAPPINGS: Dict[str, Dict[str, str]] = { # Atoms "Button": { "borderRadius": "$borderRadius.md", "fontFamily": "$typography.fontFamily.sans", "fontSize": "$typography.fontSize.sm", "fontWeight": "$typography.fontWeight.medium", "paddingX": "$spacing.4", "paddingY": "$spacing.2", }, "Input": { "borderRadius": "$borderRadius.md", "fontFamily": "$typography.fontFamily.sans", "fontSize": "$typography.fontSize.sm", "paddingX": "$spacing.3", "paddingY": "$spacing.2", "background": "$colors.surfaces.card", "borderColor": "$colors.text.muted", "color": "$colors.text.primary", }, "Label": { "fontFamily": "$typography.fontFamily.sans", "fontSize": "$typography.fontSize.sm", "fontWeight": "$typography.fontWeight.medium", "color": "$colors.text.primary", "marginBottom": "$spacing.1", }, "Badge": { "borderRadius": "$borderRadius.full", "fontFamily": "$typography.fontFamily.sans", "fontSize": "$typography.fontSize.xs", "fontWeight": "$typography.fontWeight.medium", "paddingX": "$spacing.2", "paddingY": "$spacing.1", }, "Avatar": { "borderRadius": "$borderRadius.full", "background": "$colors.brand.primary", "color": "$colors.text.inverse", }, "Icon": { "color": "currentColor", }, "Checkbox": { "borderRadius": "$borderRadius.sm", "borderColor": "$colors.text.muted", "size": "$spacing.4", }, "Radio": { "borderRadius": "$borderRadius.full", "borderColor": "$colors.text.muted", "size": "$spacing.4", }, "Toggle": { "borderRadius": "$borderRadius.full", "background": "$colors.text.muted", "height": "$spacing.6", }, "Spinner": { "color": "$colors.brand.primary", }, "Divider": { "color": "$colors.surfaces.background", }, "ProgressBar": { "borderRadius": "$borderRadius.full", "background": "$colors.surfaces.background", "fillColor": "$colors.brand.primary", "height": "$spacing.2", }, "Dot": { "borderRadius": "$borderRadius.full", "size": "$spacing.2", },

# Molecules
"FormField": {
"gap": "$spacing.1",
},
"SearchInput": {
"borderRadius": "$borderRadius.lg",
"background": "$colors.surfaces.background",
"paddingLeft": "$spacing.8",
},
"StatusIndicator": {
"gap": "$spacing.2",
"fontSize": "$typography.fontSize.sm",
},
"Breadcrumb": {
"gap": "$spacing.2",
"fontSize": "$typography.fontSize.sm",
},
"Pagination": {
"gap": "$spacing.1",
"buttonSize": "$spacing.8",
},
"EmptyState": {
"gap": "$spacing.4",
},
"Toast": {
"borderRadius": "$borderRadius.lg",
"shadow": "$shadows.lg",
"padding": "$spacing.4",
"gap": "$spacing.3",
},

# Organisms
"Header": {
"background": "$colors.surfaces.card",
"height": "$spacing.16",
"paddingX": "$spacing.6",
"shadow": "$shadows.sm",
},
"Card": {
"background": "$colors.surfaces.card",
"borderRadius": "$borderRadius.lg",
"shadow": "$shadows.card",
"padding": "$spacing.6",
},
"Modal": {
"background": "$colors.surfaces.card",
"borderRadius": "$borderRadius.xl",
"shadow": "$shadows.lg",
"padding": "$spacing.6",
},
"DataTable": {
"headerBackground": "$colors.surfaces.background",
"headerFontWeight": "$typography.fontWeight.medium",
"cellPadding": "$spacing.3",
},
"Form": {
"gap": "$spacing.6",
},
"Sidebar": {
"background": "$colors.surfaces.card",
"width": "16rem",
"paddingY": "$spacing.4",
},

# Templates
"DashboardGrid": {
"gap": "$spacing.6",
"padding": "$spacing.6",
},
"DetailView": {
"gap": "$spacing.6",
"padding": "$spacing.6",
},
"KanbanBoard": {
"gap": "$spacing.4",
"padding": "$spacing.4",
},
"FormPage": {
"maxWidth": "40rem",
"padding": "$spacing.8",
},

}

Variant-specific token overrides

VARIANT_TOKEN_MAPPINGS: Dict[str, Dict[str, Dict[str, str]]] = { "Button": { "primary": { "background": "$colors.brand.primary", "color": "$colors.text.inverse", }, "secondary": { "background": "$colors.surfaces.card", "color": "$colors.text.primary", "borderColor": "$colors.text.muted", }, "outline": { "background": "transparent", "color": "$colors.brand.primary", "borderColor": "$colors.brand.primary", }, "ghost": { "background": "transparent", "color": "$colors.text.primary", }, "destructive": { "background": "$colors.semantic.error", "color": "$colors.text.inverse", }, }, "Badge": { "success": { "color": "$colors.semantic.success", }, "warning": { "color": "$colors.semantic.warning", }, "error": { "color": "$colors.semantic.error", }, "info": { "color": "$colors.semantic.info", }, }, }

class A2UITransformer: """ Transforms A2UI trees into Protocode intermediate representation. """

def __init__(
self,
component_mappings: Optional[Dict[str, Dict[str, str]]] = None,
variant_mappings: Optional[Dict[str, Dict[str, Dict[str, str]]]] = None,
):
"""
Initialize the transformer.

Args:
component_mappings: Custom component token mappings
variant_mappings: Custom variant token overrides
"""
self.component_mappings = component_mappings or COMPONENT_TOKEN_MAPPINGS
self.variant_mappings = variant_mappings or VARIANT_TOKEN_MAPPINGS

def transform(self, a2ui_node: Dict[str, Any]) -> ProtocodeNode:
"""
Transform an A2UI node to a Protocode node.

Args:
a2ui_node: A2UI node dict

Returns:
ProtocodeNode instance
"""
node_id = a2ui_node.get("id", str(uuid.uuid4())[:8])
component = a2ui_node.get("component")
atomic_level = a2ui_node.get("type")

# Determine node type
if component:
node_type = NodeType.COMPONENT
else:
node_type = NodeType.ELEMENT

# Create base node
protocode = ProtocodeNode(
id=node_id,
node_type=node_type,
component=component,
atomic_level=AtomicLevel(atomic_level) if atomic_level else None,
)

# Process props
static_props = a2ui_node.get("props", {})
dynamic_props = a2ui_node.get("dynamicProps", {})

protocode.props.static = {
k: v for k, v in static_props.items()
if not isinstance(v, dict) or "source" not in v
}

for key, binding in dynamic_props.items():
protocode.props.dynamic[key] = DataBinding(
source=binding.get("source", "props"),
path=binding.get("path", ""),
transform=binding.get("transform"),
)

# Apply component token mappings
if component and component in self.component_mappings:
protocode.styles.token_bound = dict(self.component_mappings[component])

# Apply variant overrides
variant = static_props.get("variant")
if variant and component in self.variant_mappings:
if variant in self.variant_mappings[component]:
protocode.styles.token_bound.update(
self.variant_mappings[component][variant]
)

# Process events
for event_name, handler in a2ui_node.get("events", {}).items():
if isinstance(handler, dict):
protocode.events.append(ProtocodeEvent(
name=event_name,
handler=handler.get("action", ""),
payload=handler.get("payload"),
))
else:
protocode.events.append(ProtocodeEvent(
name=event_name,
handler=str(handler),
))

# Process children
for child in a2ui_node.get("children", []):
protocode.children.append(self.transform(child))

# Process slots
for slot_name, slot_children in a2ui_node.get("slots", {}).items():
protocode.slots[slot_name] = [
self.transform(child) for child in slot_children
]

# Add metadata
metadata_source = a2ui_node.get("metadata", {})
protocode.metadata = ProtocodeMetadata(
source=node_id,
accessibility=AccessibilitySpec(
role=metadata_source.get("accessibility", {}).get("role"),
label=metadata_source.get("accessibility", {}).get("label"),
) if "accessibility" in metadata_source else None,
responsive=metadata_source.get("responsive"),
animation=metadata_source.get("animation"),
)

return protocode

def transform_tree(self, a2ui_tree: Dict[str, Any]) -> Dict[str, Any]:
"""
Transform a complete A2UI tree to Protocode.

Args:
a2ui_tree: Complete A2UI tree (may have root "tree" key)

Returns:
Protocode tree as dict
"""
# Handle wrapped tree structure
if "tree" in a2ui_tree:
root = a2ui_tree["tree"]
else:
root = a2ui_tree

protocode = self.transform(root)

return {
"version": "1.0.0",
"generator": "coditect-a2ui-transformer",
"tree": protocode.to_dict(),
"statistics": self._calculate_statistics(protocode),
}

def _calculate_statistics(self, node: ProtocodeNode) -> Dict[str, Any]:
"""Calculate tree statistics."""
stats = {
"total_nodes": 0,
"atoms": 0,
"molecules": 0,
"organisms": 0,
"templates": 0,
"components": set(),
"tokens_used": set(),
}

def walk(n: ProtocodeNode):
stats["total_nodes"] += 1

if n.atomic_level:
if n.atomic_level == AtomicLevel.ATOM:
stats["atoms"] += 1
elif n.atomic_level == AtomicLevel.MOLECULE:
stats["molecules"] += 1
elif n.atomic_level == AtomicLevel.ORGANISM:
stats["organisms"] += 1
elif n.atomic_level == AtomicLevel.TEMPLATE:
stats["templates"] += 1

if n.component:
stats["components"].add(n.component)

for token in n.styles.token_bound.values():
if token.startswith("$"):
stats["tokens_used"].add(token)

for child in n.children:
walk(child)

for slot_children in n.slots.values():
for child in slot_children:
walk(child)

walk(node)

return {
"total_nodes": stats["total_nodes"],
"atoms": stats["atoms"],
"molecules": stats["molecules"],
"organisms": stats["organisms"],
"templates": stats["templates"],
"unique_components": len(stats["components"]),
"tokens_used": len(stats["tokens_used"]),
}

def main(): """CLI entry point.""" import argparse

parser = argparse.ArgumentParser(
description='Transform A2UI trees to Protocode',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""

Examples:

Transform A2UI to Protocode

python a2ui_transformer.py input.a2ui.json -o output.protocode.json

Transform and resolve tokens

python a2ui_transformer.py input.a2ui.json --design-system design-system.json

Pretty print to stdout

python a2ui_transformer.py input.a2ui.json --pretty """ )

parser.add_argument('input', help='Input A2UI JSON file')
parser.add_argument('--output', '-o', help='Output Protocode JSON file')
parser.add_argument('--design-system', '-d',
help='Design system for token resolution')
parser.add_argument('--pretty', '-p', action='store_true',
help='Pretty print output')

args = parser.parse_args()

try:
# Load A2UI
with open(args.input, 'r', encoding='utf-8') as f:
a2ui = json.load(f)

# Transform
transformer = A2UITransformer()
protocode = transformer.transform_tree(a2ui)

# Resolve tokens if design system provided
if args.design_system:
from token_resolver import TokenResolver
resolver = TokenResolver(args.design_system)
protocode = resolver.resolve_all(protocode)

# Output
indent = 2 if args.pretty else None

if args.output:
with open(args.output, 'w', encoding='utf-8') as f:
json.dump(protocode, f, indent=indent)
print(f"Protocode written to: {args.output}")
else:
print(json.dumps(protocode, indent=indent))

return 0

except FileNotFoundError as e:
print(f"Error: {e}", file=sys.stderr)
return 1
except json.JSONDecodeError as e:
print(f"Invalid JSON: {e}", file=sys.stderr)
return 1

if name == 'main': sys.exit(main())