Skip to content

The Four Graphs

HBIA's frontend architecture decomposes the UI into four interconnected reactive graphs. This page covers each graph type in detail.

UI Graph

The UI Graph represents the component hierarchy as a tree of pure render nodes.

UINode

from honey_badgeria.front.graph import UINode

node = UINode(
    name="Dashboard",
    view="dashboard_view",
    reads=["FilterState.dateRange", "ChartState.series"],
    events={"refresh": "refresh_chart_event"},
    children=["ChartHeader", "ChartCanvas", "ChartLegend"],
    props={"variant": "full"},
)
Field Type Description
name str Component name (PascalCase)
view str View function/file name
reads list[str] State fields this component subscribes to
events dict[str, str] Events this component can emit: {action: event_name}
children list[str] Child component names
props dict[str, str] Static props

The Pure Component Rule

UI nodes are pure render functions. This is a strict rule:

  • No useState — State lives in the State Graph.
  • No useEffect — Effects live in the Effect Graph.
  • No useContext — Data comes via reads (state subscriptions).
  • No side effects — Everything is declared externally.

A component receives data through reads and emits user interactions through events. It does nothing else.

UIGraph

from honey_badgeria.front.graph import UIGraph

ui = UIGraph()
ui.add_node(node)

root = ui.root                    # Root component name
subtree = ui.get_subtree("Dashboard")  # BFS descendants
all_reads = ui.all_reads()        # All state references across all nodes
all_events = ui.all_events()      # All events emitted across all nodes

State Graph

The State Graph represents state ownership — what data exists, what types it has, how it can change, and what effects it triggers.

StateNode

from honey_badgeria.front.graph import StateNode

node = StateNode(
    name="FilterState",
    interface="FilterStateData",
    fields={"dateRange": "DateRange", "region": "string", "category": "string"},
    mutations=["set_date_range", "set_region", "set_category", "reset_filters"],
    triggers=["fetch_chart_data", "update_url_params"],
    initial={"region": '"all"', "category": '"all"'},
)
Field Type Description
name str State node name (PascalCase + "State")
interface str TypeScript interface name for the generated code
fields dict[str, str] Field names and TypeScript types
mutations list[str] Named mutation operations
triggers list[str] Effect names triggered on state change
initial dict[str, str] Default field values

Key Concepts

Fields are the typed data the state holds. Each field has a name and a TypeScript type:

fields:
  dateRange: DateRange     # Custom type
  region: string           # Primitive
  loading: boolean
  items: ChartItem[]       # Array type

Mutations are the only way to modify state. They are named operations that the Event Graph and Effect Graph dispatch:

mutations:
  - set_date_range
  - set_region
  - reset_filters

Triggers connect state changes to effects. When any field in this state changes, the listed effects are invoked:

triggers:
  - fetch_chart_data       # Runs when FilterState changes

StateGraph

from honey_badgeria.front.graph import StateGraph

state = StateGraph()
state.add_node(node)

all_fields = state.all_fields()         # {state_name: [field_names]}
all_mutations = state.all_mutations()   # {state_name: [mutation_names]}
all_triggers = state.all_triggers()     # {state_name: [effect_names]}

# Resolve a field reference
state_name, field = state.resolve_field("FilterState.dateRange")
# ("FilterState", "dateRange")

Effect Graph

The Effect Graph represents side effects — operations triggered by state changes or events that interact with the outside world.

EffectNode

from honey_badgeria.front.graph import EffectNode

node = EffectNode(
    name="fetch_chart_data",
    watch=["FilterState.dateRange", "FilterState.region"],
    on_event=["chart_refresh_clicked"],
    handler="handlers/fetch_chart_data",
    effect_type="side_effect",
    mutates=["ChartState", "NotificationState"],
    debounce_ms=300,
)
Field Type Description
name str Effect identifier
watch list[str] State fields that trigger this effect on change
on_event list[str] Events that trigger this effect
handler str Path to handler implementation
effect_type str "side_effect" or "derived"
mutates list[str] State nodes this effect can write to
debounce_ms int \| None Optional debounce in milliseconds

Effect Types

side_effect — Interacts with the outside world: API calls, analytics, localStorage, WebSocket messages.

derived — Computes new state from existing state. Like a computed property, but explicitly declared.

Trigger Mechanisms

Effects fire in two ways:

  1. Watch — State field changes trigger the effect. When FilterState.dateRange changes, fetch_chart_data fires.
  2. On Event — User events trigger the effect. When chart_refresh_clicked occurs, the effect fires.

Mutation Targets

The mutates field declares which state nodes an effect can write to. This creates explicit, traceable data flow:

effect: fetch_chart_data
mutates:
  - ChartState          # This effect can modify ChartState
  - NotificationState   # ...and NotificationState

Debounce

Optional debounce prevents rapid-fire effects. Useful for search-as-you-type, resize handlers, and filter changes:

debounce_ms: 300    # Wait 300ms after last trigger before executing

EffectGraph

from honey_badgeria.front.graph import EffectGraph

effects = EffectGraph()
effects.add_node(node)

# Find effects watching a state
watching = effects.effects_for_state("FilterState")

# Find effects watching a specific field
watching = effects.effects_for_field("FilterState.region")

# Find effects triggered by an event
triggered = effects.effects_for_event("chart_refresh")

# Get mutation targets
targets = effects.mutation_targets()  # {effect_name: [states]}

# Full propagation chain from a state
chain = effects.trigger_chain("FilterState")

Event Graph

The Event Graph maps user interactions to ordered sequences of state mutations.

EventNode

from honey_badgeria.front.graph import EventNode

node = EventNode(
    name="refresh_chart_clicked",
    flow=["FilterState.reset_filters", "ChartState.set_loading"],
    source="ChartHeader",
    guards=["is_authenticated", "has_permission"],
)
Field Type Description
name str Event identifier
flow list[str] Ordered list of mutations or effect triggers
source str \| None Component that emits this event
guards list[str] Preconditions that must be true

Event Flow

The flow field defines an ordered mutation sequence. When the event fires:

  1. Check all guards.
  2. Execute FilterState.reset_filters (first mutation).
  3. Execute ChartState.set_loading (second mutation).
  4. Any triggered effects run after mutations complete.

Guards

Guards are preconditions. If any guard fails, the event doesn't execute:

guards:
  - is_not_loading         # Don't refresh while already loading
  - is_authenticated       # Must be logged in

EventGraph

from honey_badgeria.front.graph import EventGraph

events = EventGraph()
events.add_node(node)

# Find events affecting a state
affecting = events.events_for_state("ChartState")

# Find events from a component
from_source = events.events_from_source("ChartHeader")

FrontendDomain

The FrontendDomain bundles all four graphs into a single domain:

from honey_badgeria.front.graph import FrontendDomain

domain = FrontendDomain(
    name="dashboard",
    ui_graph=ui,
    state_graph=state,
    effect_graph=effects,
    event_graph=events,
)

A domain represents a logical section of your application (e.g., "dashboard", "auth", "settings"). Each domain has its own set of four graphs.

The Complete Data Flow

1. User clicks a button in a UINode
   → UINode.events["refresh"] = "refresh_chart_event"

2. EventGraph dispatches "refresh_chart_event"
   → flow: [ChartState.set_loading, FilterState.reset_filters]

3. StateGraph mutations execute
   → ChartState.loading = true
   → FilterState fields reset

4. State changes trigger EffectGraph
   → FilterState triggers: [fetch_chart_data]
   → fetch_chart_data watches FilterState.dateRange, FilterState.region

5. Effect executes (API call)
   → fetch_chart_data handler runs
   → mutates ChartState (new series data)

6. UIGraph re-renders
   → Dashboard reads ChartState.series → updated chart
   → Dashboard reads ChartState.loading → spinner hidden

Every step is declared in YAML. An AI agent can trace this entire flow from user action to UI update by reading four files.