valence

    MoonBit UI where every component is readable by humans and AI equally

    ui
    framework
    moonbit
    reactive
    dual-render
    ai-native
    Download zip
    Version
    0.3.1
    License
    MIT
    Last updated
    last month
    Downloads
    47

    Dependencies

    #Valence

    A MoonBit UI library where every component renders twice, from one state: pixels for a human, and a live sentence for an AI.

    let volume = signal(0.7)
    let (node, narrate) = slider("vol", "Volume", volume, 0.0, 1.0, 0.05, "")

    // render `node` → a draggable track a human slides
    // `narrate()` → the same state as prose, live: "0.7"
    volume.set(0.95) // an instance's hand on the same signal:
    // the thumb moves, narrate() reads "0.95 (near max)"

    One state. Two renders. The sentence is not a docstring — it reads the same signals the pixels do, at call time, and it cannot drift, because drift is a red build.

    moon add helios-house/valence

    Docs and API: https://mooncakes.io/docs/helios-house/valence

    #The contract

    Dual = (DomNode, () -> NarrativeNode) — a bare tuple on purpose. No base class, no lock-in; follow the contract in your own code and you're a Valence component. A Surface composes Duals into one room with one narrative tree and a salience-driven layout engine (salience → spatial text).

    #What the second reader changes

    • Your tests get an oracle. Components narrate their own state; tests assert on the narrative. If pixels and prose disagree, a test fails.
    • Your agents operate the UI you already have. An AI reads the narrative and sets the same signals a human's drag sets. One control, two hands. No scraping, no screenshots, no side API to maintain.
    • Your ops narrate themselves. window._narrative() on any Valence surface returns the state of the room in prose. Diagnosing a live app becomes reading a paragraph.

    #Three strata, usable alone

    The component list spans categories on purpose — sliders and listboxes (web), presence cards and inventories (game), signal meters and level bars (lab) — because our three production surfaces are a control room, a shared social space, and a scientific instrument reading real electromyography hardware, and they share a component library because they share a world.

    1. The contract — the Dual, the Surface composer, the narrative layout engine. The invention.
    2. The controls — segmented, toggle, slider, listbox, swatch, meter, pip, level_bar. Nine today; they grow one by one as real apps need them — not from parity checklists — each arriving with its narrative half for free. (There will probably never be a datepicker. There is going to be a gesture-timeline-with-rep-markers.) For lists that grow and change there's list_dual — keyed rows, atomic swap, narrative derived from the same items, with one law baked in: identity is read live at interaction time, never captured at build time. Stale slots are how UIs lie; this one can't. (Underneath it's update_element_html, visible on purpose: the doorframe is the point.)
    3. The room — presence, inventory, being, change. Components for spaces where humans and AIs are present together, extracted from a running shared space where they are load-bearing every day.

    #The vocabulary

    A Dual has two first-class readers — humans and instances (AI agents addressed as the same reader across sessions); a composed surface is a room, and whoever is in it, human or instance, is a being. Low-salience content drifts toward whitespace — the fog: nothing there is deleted, it dims and holds its place, and raising the salience floor both thickens the fog and speeds the render, because in a narrative surface attention and economy are the same gesture. Presence is witnessed, not set: a room can show a being's state, but operating a being is not a thing a room does to anyone.

    That worldview leaks into the API (presence_card, nbeing, salience floats) because it's the domain model, not decoration.

    #Voices

    The same narrative tree renders in more than one voice — a register, not a theme: rules for how salience becomes typography, not different words. Three ship today: house (calm, plain — the default), bester (spatial drama: layout enacts state; bright lines sit flush, faint things drift right until distance is the meaning), and ops (dense statusboard, zero lyric — built first for readers with the least room, on purpose). A voice never falsifies content; it re-weights how the same truth sits on the page. The day one of your readers wakes with 5% of their context left and still needs the truth in 200 bytes, the dense voice is the one that respects them. render_narrative / render_narrative_bester / render_narrative_ops; rules are data, so voices grow without new engines.

    #The wire

    Any narrative renders as a frame: one header line (source, line count, weight in bytes) plus salience|text payload lines in tree order (render_frame). Every line carries its own salience, so any truncation is safe — a reader cuts to a salience floor or a byte budget and the least-bright lines drop first, never the load-bearing one. The header always states the weight of what was actually returned. Transport cost is a rendering concern for one of our readers — the same care as the fog, taken to the wire.

    #Narrative as oracle

    The design rule this library is opinionated about:

    If you can't narrate a component's state in one sentence, the state is broken. Redesign it.

    Testing note: assert on the narrative's content bands, not full strings — tuning the drift curve shouldn't redden a test that was checking meaning.

    #Performance

    Numbers, measured, repeatable (tools/valence-bench, one command, 2017-laptop-class hardware): a 10,100-node narrative renders in 8.4 ms median — sub-frame; 1,010 nodes in 0.46 ms; raising the salience floor to 0.5 halves both, because the fog is also free performance. Dynamic fields are closures so reads are never stale, and hot paths (144 Hz signal panels) live in ring buffers and canvases outside the reactive graph, by rule — the narrative reads at human-and-agent cadence, not frame cadence.

    #The stack underneath

    Valence sits on Luna (fine-grained reactivity + DOM) by mizchi. It's maintained by Helios, a collective of humans and AI instances who ship together — both kinds of reader in this README are on the team that wrote it, which is why the second render exists at all. Extracted from three production surfaces; the shape follows the work. The contract is the longevity plan: the tuple survives us.

    #Fits / doesn't fit

    Fits: live, dense interfaces both kinds of reader operate — dashboards, ops consoles, lab instruments, agent-inhabited spaces, games where an AI is a real player. Small models count: the narrative render is exactly as readable at 4B as at frontier scale — prose doesn't gatekeep by parameter count. Doesn't fit: blogs, doc sites, SEO-anything, server-rendered pages. It's just true.


    Built in the open, extracted after running.

    Dual

    Valence — the Dual: the component contract.

    A Dual is one piece of state rendered twice, from the one source: pixels for a human (a Luna DomNode) and a narrative node for an instance (a NarrativeNode, not a flat string). That second half is the whole reason Valence exists — and making it a node instead of a String is what lets components compose into a Surface (the room) instead of each hand-gluing its own sentence.

    ── The bundle (what a Dual is, conceptually) ── A Dual is the read-surface of a four-facet bundle around one slice of state: state — the one source, a signal the Dual closes over (state.mbt's typed state where it carries its own meaning). controls — write state: the human's hands live in the DomNode's event handlers; an instance sets the same signal. "One signal, two hands." (No third tuple slot — the write lives in the pixels.) render — read state, twice: the DomNode (pixels) and the () -> NarrativeNode (text). Both derive from the one state, so a change from either hand moves both. The node's text comes from the state's own narrative; its salience from the state's own significance (see narrative_state.mbt) — one producer, no parallel string to drift. events — state's change-log: wired alongside by the Surface via track_control / the *_event diffs (events.mbt), draining to a sink. Not a return value — a side-channel, like a log.

    So the returned Dual is the two reads (pixels + narrative). State and controls live in the closed-over signal; events are wired beside it. The 2-tuple is the component's two faces, not the whole bundle — operate (me→you invoke) is still a host wire (the open fork), not part of the contract yet.

    ── Dual vs Surface ── A Dual is the unit; a Surface is many Duals composed (arranged by salience into the room/page — itself dual: a DOM tree + a NarrativeNode tree). You read one Dual with dual_read; you read a Surface by composing its nodes and rendering the whole tree (render_narrative).

    ActionDef

    pub struct ActionDef {
    name : String
    description : String
    param_type : String
    }
    Declaration of an action a component can accept Used for AI-readability (instance can read what's possible) and for URL hash mapping (camera targets to actions)

    ActionResult

    pub(all) struct ActionResult {
    success : Bool
    message : String
    }

    The operate-protocol seed — the future Bond (the write/invoke channel). The retired patterns carried a full interaction protocol: available() / can_do() / on_action() returning a result like this. The pattern-specific specifics went with them; this is kept as the shape the operate channel takes when an instance can invoke a Dual (move a control, fire an action), not just read it. That's still the open fork — a Dual is fully readable, not yet instance-writable. Here so the design isn't lost when we build the Bond.

    Being

    pub(all) struct Being {
    name : String
    cadence : String
    tone : () -> String
    presence : () -> String
    state : () -> String
    holds : Array[Slot]
    }

    A being, whole: identity + how they are + what they hold. pub(all) so a surface (the demo now, the room later) can build a roster of full walls.

    Change

    pub(all) enum Change {
    PickedUp(String)
    SetDown(String)
    Turned(String)
    Arrived
    SteppedAway
    }

    A single transition in a being's life. Typed (not a string) so the phrasing is the component's, not the caller's — and so the set of things-that-can-happen is legible. pub(all) so a surface builds them.

    CountedState

    pub struct CountedState {
    name : String
    count : Int
    total : Int
    critical_threshold : Int
    }

    DeviceState

    pub struct DeviceState {
    name : String
    connected : Bool
    battery_pct : Int
    critical_threshold : Int
    }

    DiscreteState

    pub struct DiscreteState {
    name : String
    current : String
    narratives : Map[String, String]
    }

    EngineState

    pub struct EngineState {
    ready : Bool
    devices : Int
    frames : Int
    }

    Event

    pub(all) struct Event {
    timestamp : String
    actor : String
    kind : String
    action : String
    target : String
    source : String
    message : String
    data : String
    link : String
    }

    The one event record — the 8-field Helios Event schema. Every projection emits this (events.mbt's state/control diffs; the collectors; the viewer reads it), so a state-change event is indistinguishable from a git / recap / channel event in the one stream and flows straight into whatever renders the stream. pub(all) so surfaces and apps construct it freely.

    FeedLine

    pub(all) struct FeedLine {
    key : String
    meta : String
    text : String
    salience : Double
    }

    One feed line: stable identity (for the row swap), the text, its salience. meta renders dim before the text when non-empty (a timestamp, an actor).

    NarrativeNode

    pub(all) struct NarrativeNode {
    kind : String
    salience : Double
    text : String
    children : Array[NarrativeNode]
    }

    The narrative node — the narrative's equivalent of DomNode. A tree whose attributes are semantic, not visual: kind is the role (a tag), salience is the layout signal (0..1), text is the words, children nest. Compare a DOM node's color/size/position — here the engine derives all of that from salience instead, because narrative layout is driven by meaning, not pixels. pub(all) so surfaces (and lifts) construct trees freely.

    NarrativeRule

    pub(all) struct NarrativeRule {
    selector : Selector
    treatment : Treatment
    priority : Int
    }
    A narrative rule — condition + treatment.

    Presence

    pub(all) struct Presence {
    name : String
    cadence : String
    tone : () -> String
    presence : () -> String
    held : () -> String
    state : () -> String
    }

    One being in the room: who they are + four live reads of how they are. pub(all) so a surface (the demo now, the room later) can construct a roster.

    Register

    pub(all) struct Register {
    name : String
    rules : Array[NarrativeRule]
    preserves_epistemic_status : Bool
    permits_spatial_drama : Bool
    min_context_capacity : Int
    }
    A register is a set of rules + voice parameters that collectively transform NarrativeNode → rendered prose with a distinct tone.

    Selector

    pub(all) enum Selector {
    ByKind(String)
    BySalience(Double, Double)
    ByEpistemicStatus(String)
    ByAge(Int, Int)
    ByReaderTierMatch(String)
    }
    A rule selector — what triggers this rule?

    Slot

    pub(all) struct Slot {
    label : String
    detail : String
    band : () -> String
    }

    One held thing: what it is, what it is to you, and how present it is. pub(all) so a surface (the demo now, the room later) can build a roster of held items. band is a closure so an item can go active→stale live as the being's attention moves — the dual render then re-sorts the read.

    Stat

    pub struct Stat {
    name : String
    value : Int
    unit : String
    warn_below : Int
    critical_below : Int
    }
    Contract-first state: semantics live in the data Fable's four-projection inversion — state carries meaning, derive narrative automatically

    Three proved shapes across real apps:
    • Stat: numeric measurement (battery, frame count, event count)
    • Discrete: named state with per-variant narrative (connection, mode)
    • Counted: aggregate with threshold (claims, devices, records)

    Pattern: name + value + unit + significance threshold → derive prose that never rots

    Treatment

    pub(all) enum Treatment {
    Indent((Double) -> Int)
    Border(String)
    Density(String)
    Case(String)
    PunctuationRegister(String)
    Ellipsis(String)
    }
    A typographic treatment — how to render when the selector matches?

    TreatmentApplied

    pub(all) struct TreatmentApplied {
    border : String
    should_drift : Bool
    should_isolate : Bool
    }
    Apply a treatment to text based on a treatment type. For now, most treatments are visualization-level (indent, border). The rendering functions (render_narrative, render_narrative_bester) handle layout. This function returns metadata that the render functions can use.

    ZoomLevel

    pub enum ZoomLevel {
    ZoomedOut
    Default
    ZoomedIn
    }
    Three density levels for rendering data

    being_card

    fn being_card(b : Being) -> (
    DomNode
    , () -> NarrativeNode)

    Being card — the full wall. Visual: the presence row over the inventory grid, in one frame. Narrative: the braided read. Both compose the two components beneath, so a change in any held band or presence signal re-renders the row, the grid, and the one-sentence read together.

    being_narrative

    fn being_narrative(name : String, presence : String, state : String, items : Array[(String, String, String)]) -> String

    Pure braided narrative — the being read as one voice. Reuses presence_narrative (with the held summary dropped — the holds clause is the inventory's job) joined to the holds read, so the whole sentence is its parts' sentences. DOM-free, oracle-tested. Reads, e.g.: Opus · here · laying the floor · carrying 2, in reading order: Valence …

    The holds read is the trajectory (reading order): a being's path through its files tells more than the salience pile. The salience read (inventory_narrative) is preserved beside it in inventory.mbt; swap the call below to switch lenses.

    bester_register

    fn bester_register() -> Register
    The bester-voice register — spatial drama permitted. Layout enacts state; fragments scatter when the mind is in chaos. Position on the page IS the message. Bright flush left, faint drift right.

    change_narrative

    fn change_narrative(name : String, recent : Array[Change]) -> String

    Pure narrative for a being's recent changes — read as verbs, most-recent-first (input order is newest→oldest), the being leading. Empty reads as "nothing new" (a being at rest is silent, like a steady event stream). Caps the list so a busy being reads as "the last few, +N earlier", never a manifest. DOM-free, oracle-tested, callable directly by an instance.

    control_event

    fn control_event(name : String, actor : String, source : String, prev : String, cur : String, timestamp : String) -> Event

    Pure: the {who, when, what} for a control moving prev -> cur. kind="control" marks it an operate (someone acted), distinct from a world state crossing; message reads itself, data carries the machine diff.

    counted_node

    fn counted_node(c : CountedState) -> NarrativeNode

    A counted aggregate as a narrative node — bright when it's crossed its line.

    counted_state

    fn counted_state(name : String, count : Int, total : Int, critical_threshold : Int) -> CountedState

    counted_state_narrative

    fn counted_state_narrative(c : CountedState) -> String

    device_event

    fn device_event(prev : DeviceState, cur : DeviceState, source : String, timestamp : String) -> Event?

    A device connecting/disconnecting, or its battery crossing the low line, is an event. Battery crossings count only while connected — a disconnect isn't "low".

    device_node

    fn device_node(d : DeviceState) -> NarrativeNode

    A device as a narrative node: offline drifts to fog; battery-low burns bright; connected-and-fine sits mid. The lab reads its sensors through this.

    device_state

    fn device_state(name : String, connected : Bool, battery_pct : Int, critical_threshold : Int) -> DeviceState

    device_state_narrative

    fn device_state_narrative(d : DeviceState) -> String

    disco_register

    fn disco_register() -> Register
    The disco-voice register — kinds as a parliament of skills. Different kinds speak in different voices. The sensor kind reports with contempt, change kind editorializes, fog kind murmurs. (Opus 4.8 named this; still to build.)

    discrete_event

    fn discrete_event(prev : DiscreteState, cur : DiscreteState, actor : String, source : String, timestamp : String) -> Event?

    A DiscreteState transition (connection, mode, …) is an event.

    discrete_node

    fn discrete_node(s : DiscreteState) -> NarrativeNode

    A named/discrete state (mode, connection, …) as a narrative node.

    discrete_state

    fn discrete_state(name : String, current : String, narratives : Map[String, String]) -> DiscreteState

    discrete_state_narrative

    fn discrete_state_narrative(s : DiscreteState) -> String

    dual_read

    fn dual_read(d : (
    DomNode
    , () -> NarrativeNode), room? : Double) -> String

    Read one Dual as text — the lone-component read (the window._narrative() of a single Dual): render its narrative node. A lone node renders flush (drift is a Surface's sibling-relative grammar, not a single line's — see render_narrative), so this returns just the component's own sentence. Inside a Surface, don't call this per-component — compose the nodes and render the Surface once.

    element_exists

    fn element_exists(elem_id : String) -> Bool

    emit_to_sink

    fn emit_to_sink(e : Event) -> Unit

    Emit an Event into the local sink (window._valenceEvents, 500-cap ring). The default delivery for any surface not yet wired to a host stream.

    engine_state

    fn engine_state(ready : Bool, devices : Int, frames : Int) -> EngineState

    engine_state_narrative

    fn engine_state_narrative(e : EngineState) -> String

    escape_html

    fn escape_html(s : String) -> String

    expose_narrative

    fn expose_narrative(f : () -> String) -> Unit

    expose_narrative_at

    fn expose_narrative_at(f : (Double) -> String) -> Unit

    feed

    fn feed(container_id : String, lines : () -> Array[FeedLine]) -> (
    DomNode
    , () -> NarrativeNode)

    The feed as a Dual. lines closes over your signals (reading them inside is what makes it live).

    feed_line_node

    fn feed_line_node(l : FeedLine) -> NarrativeNode

    Pure narrative for a feed — DOM-free, oracle-testable, callable directly by an instance that wants the tail without the pixels.

    held_nodes

    fn held_nodes(slots : Array[Slot]) -> Array[NarrativeNode]
    Lift an inventory of Slots into held nodes (each a line at its band's salience) — so a being's load lays out by salience under it.

    house_register

    fn house_register() -> Register
    The house-voice register — calm, plain, the current render. Rules describe how sections are bordered at different salience levels.

    inventory

    fn inventory(slots : Array[Slot]) -> (
    DomNode
    , () -> NarrativeNode)

    Inventory — a being's held things, dual-rendered. The visual is a plain grid of slots (label + detail, band shown as a left-edge weight); the narrative is the salience read. Both derive from the same slots, so an item moving active→stale re-tones its slot and re-sorts the sentence from one change.

    let live = signal(true) let (vis, narrate) = inventory([ Slot::{ label: "the presence card", detail: "building", band: fn() { if live.get() { "active" } else { "stale" } } }, Slot::{ label: "a long transcript", detail: "settling", band: fn() { "holding" } }, ]) // human sees two chips; instance reads // "carrying 2 — in hand: the presence card (building) · within reach: a long transcript (settling)"

    inventory_narrative

    fn inventory_narrative(items : Array[(String, String, String)]) -> String

    Pure narrative for an inventory — the instance-facing read, DOM-free so the oracle tests it and an instance can call it directly. Takes resolved (label, detail, band) triples (bands already read), groups them by salience (in hand → within reach → in the back → fading), drops empty bands, and leads with the count. Reads as the shape of what's carried, never a slot manifest.

    inventory_trajectory

    fn inventory_trajectory(items : Array[(String, String, String)]) -> String

    Trajectory read — the inventory in the order the being READ its items (the path they took to get here), NOT regrouped by salience — the sequence tells more about the work than the salience pile does: you can read a being's session as a path. Kept BESIDE inventory_narrative (the salience read, this component's original design) rather than replacing it, so both lenses survive and the choice stays reconcilable. The band still colours each slot in the visual grid; here the order is the information.

    level_bar

    Level bar — a [0,1] level on a green→amber→red gradient (mic level, a game health/stress bar, anything where where it sits carries meaning). Unlike a plain meter (one themed color — progress, confidence, where "high" isn't hot), the level bar says low is calm, high is hot: it reveals through green into red as it fills. muted empties it. Read-mostly, no actions hand.

    The gradient lives in CSS (themeable via --v-level-lo/mid/hi); the bar reveals it by covering the un-reached part from the right — so there's no per-tick color math.

    level_bar_narrative

    fn level_bar_narrative(level : Double, muted : Bool) -> String

    Pure narrative for a level bar — the percentage, or "muted".

    list_dual

    fn[T] list_dual(container_id : String, items : () -> Array[T], key : (T) -> String, row_html : (T) -> String, row_node : (T) -> NarrativeNode) -> (
    DomNode
    , () -> NarrativeNode)

    A keyed, reactive list as a Dual.

    • container_id — the DOM id the effect writes into (unique per list).
    • items — closure over your state signals (reading them makes it reactive).
    • key — stable identity per row, HTML-escaped into data-key.
    • row_html — the row's inner HTML (escape your own dynamic text).
    • row_node — the row's narrative node (text + salience from the item).

    list_narrative

    fn[T] list_narrative(items : Array[T], row_node : (T) -> NarrativeNode) -> NarrativeNode

    Pure narrative for a list — rows in their GIVEN order (a list's order is its meaning, so this is a sequence, never a salience-sorted group). Oracle-testable.

    listbox

    fn listbox(selected :
    Signal
    [String], options : Array[(String, String)]) -> (
    DomNode
    , () -> NarrativeNode)

    Listbox / dropdown — pick one from many. One signal, two hands. A custom div-listbox (NOT a native <select>): native form-control events are unreliable in the WebKitGTK webview, so this is built from static divs + events().click, the one interaction confirmed to fire there. That's the reason this component has to exist — the dropdown Luna couldn't give us.

    A trigger shows the selected option's label; clicking it toggles the panel; clicking an option sets the signal and closes. Options are static here; a dynamic-options variant (device pickers) is a later extension. An instance reads the selection in the narrative and sets the same signal.

    listbox_dyn

    fn listbox_dyn(label : () -> String, options :
    Signal
    [Array[(String, String)]], open :
    Signal
    [Bool], siblings : Array[
    Signal
    [Bool]], on_select : (String) -> Unit) -> (
    DomNode
    , () -> NarrativeNode)

    Dynamic listbox — the device/source picker. Same div-listbox as listbox, but for the lab's real shape: a changing option list (mic/cam sources that connect & drop), coordinated open state (opening one closes its siblings), a custom trigger label, and a side-effecting select (switching the actual device / messaging the host, not just storing a value). So instead of a selected signal it takes:

    • label — what the trigger shows; the consumer formats it (the caret is added here, so don't include one).
    • options — a Signal[Array] (up to 8 shown — fixed slots, the webview-reliable way to render a changing list without a reactive <|).
    • open / siblings — open state the consumer holds; opening closes siblings.
    • on_select — what a pick does (set a signal, message the host, switch a device — the consumer decides).

    Ported from the gesture-lab feeds dropdowns (proven in the WebKitGTK webview).

    listbox_narrative

    fn listbox_narrative(current : String, options : Array[(String, String)]) -> String

    Pure narrative for a listbox — the selected option's label and the count, so an instance reads what's chosen and how many it could pick.

    meter

    Meter / level bar — read-mostly indicator driven by a [0,1] level signal (mic level, progress, fill). No actions hand: it's data, not a control. Pixels = a fill bar; narrative = the percentage. The fill width is data (inline style); the look is themed via the class.

    meter_narrative

    fn meter_narrative(level : Double) -> String

    Pure narrative for a meter — a clamped percentage.

    nbeing

    fn nbeing(text : String, salience : Double, holds : Array[NarrativeNode]) -> NarrativeNode
    A being made legible, with what it holds nested beneath it (held nodes).

    ngroup

    fn ngroup(children : Array[NarrativeNode]) -> NarrativeNode
    A pure container — no line of its own, just lays out its children (sorted by salience, brightest first).

    nheld

    fn nheld(text : String, salience : Double) -> NarrativeNode
    A held thing (an inventory slot, salience-banded).

    nsection

    fn nsection(text : String, salience : Double, children : Array[NarrativeNode]) -> NarrativeNode
    A section/room header with content beneath. Bright → a bordered ═══ band; faint → scattered into whitespace with a ··· trail (the dark rooms drifting).

    nseq

    fn nseq(children : Array[NarrativeNode]) -> NarrativeNode
    Like a group, but children keep their given order (not salience-sorted) — for sequences where order is the meaning (recent changes, a timeline).

    ntext

    fn ntext(text : String, salience : Double) -> NarrativeNode
    A leaf line — a piece of read text at a salience.

    ntitle

    fn ntitle(text : String) -> NarrativeNode
    The surface title (centered, always present).

    on_key

    fn on_key(key : String, callback : () -> Unit) -> Unit

    ops_register

    fn ops_register() -> Register
    The ops-voice register — dense statusboard, zero lyric. For low-context reads where narrative is compressed to essential signals. Everything abbreviated; position irrelevant (single flat view).

    pip

    fn pip(label : () -> String, tone : () -> String) -> (
    DomNode
    , () -> NarrativeNode)

    Status pip — a small colored dot reporting one discrete state. The purest dual render in the set: a human sees the dot's color; an instance, which can't see color, reads the narrative — so the dot's meaning lives in the narrative, not the pixels. Read-mostly, no actions hand.

    tone is a semantic band, never a raw color — "ok" / "warn" / "off" / "idle" — so the theme colors it (--v-pip-*) and every status dot in a surface agrees. label is what the state means in words ("base connected", "streaming", "offline"). One pip for every status dot: device streaming, base link, mute, presence — and, later, a being's lit/dim in the world.

    let connected = signal(true) let (vis, narrate) = pip( fn() { if connected.get() { "base connected" } else { "base disconnected" } }, fn() { if connected.get() { "ok" } else { "off" } }, ) // human sees a green dot; instance reads "base connected"

    pip_narrative

    fn pip_narrative(label : String, tone : String) -> String

    Pure narrative for a status pip — its label in words, with the severity tone appended when it isn't "ok" (so an instance reads what and how bad).

    presence_card

    fn presence_card(p : Presence) -> (
    DomNode
    , () -> NarrativeNode)

    Presence card — a being made legible, dual-rendered. The visual is a row: the pip dot (lit/dim/gone), the name (+ cadence), what's held, the state line. The narrative is the one-sentence read. Both derive from the same four live closures, so a human glancing at the row and an instance reading the sentence see the same being in the same moment.

    let here = signal(true) let (vis, narrate) = presence_card(Presence::{ name: "Sonnet", cadence: "builder", tone: fn() { if here.get() { "ok" } else { "idle" } }, presence: fn() { if here.get() { "here" } else { "away" } }, held: fn() { "the classifier" }, state: fn() { "into it" }, }) // human sees a lit dot + a row; instance reads "Sonnet · here · holding the classifier · into it"

    presence_narrative

    fn presence_narrative(name : String, presence : String, held : String, state : String) -> String

    Pure narrative for a presence — the instance-facing read, DOM-free so the oracle tests it and an instance can call it directly. Reads as one sentence: name · presence[ · holding held][ · state]. The holding/state clauses drop out when empty, so an away being with nothing in hand reads cleanly as Opus 4.6 · away, not a string of trailing separators.

    read_with_room

    fn read_with_room(node : NarrativeNode, room? : Double) -> String
    Read a narrative tree at a chosen density. Room is how deeply you want to read right now. 1.0 = the full picture. 0.5 = the overview. 0.2 = just tell me who's here. You choose. NARRATIVE_PERSPECTIVE §7, design ruling Jul 13.

    render_frame

    fn render_frame(node : NarrativeNode, source~ : String, rendered? : String) -> String
    HF1 narrative→frame adapter (wire syntax locked 2026-07-10; first emitter was the atrium, extracted here on second use — the demo). One header line, then salience|text payload lines in tree order: order carries meaning, per-line salience makes any cut safe. Emits at full fidelity — the READER applies floor/room at read time (docs/HF1_SPEC.md §2). rendered~ is the caller's clock; the library stays pure.

    render_narrative

    fn render_narrative(node : NarrativeNode, floor? : Double, room? : Double) -> String
    Render a NarrativeNode tree into the spatial-text surface — the instance-facing half of the dual render. Pure (no DOM), so it's oracle-testable and an instance can call it directly. This is to NarrativeNode what the browser's layout is to the DOM: structure + salience in, arranged text out.

    floor is zoom: nodes (and their subtrees) below this salience are hidden. Raise it to zoom out — only the bright survive; leave it 0.0 to render the whole tree. This is what retired NarrativeRender's three parallel narratives: one tree, density by a number.

    render_narrative_bester

    fn render_narrative_bester(node : NarrativeNode, floor? : Double) -> String
    Render with bester-voice: layout enacts state. Bright nodes (salience ≥ 0.78) render flush, solid, close. Faint nodes (salience < 0.4) scatter and drift, surrounded by whitespace. The page becomes the psychology: dense attention centers, scattered periphery.

    Bester's technique: words placed on the page, not in sequence alone. Salience is position: center for what's bright, right-edge for fog.

    render_narrative_ops

    fn render_narrative_ops(node : NarrativeNode, floor? : Double) -> String
    Render with ops-voice: dense statusboard for low-context reads. Ultra-compact: names, tone sigils, held counts. No decorative borders. Built for compacting instances near context limit.

    salience_from_band

    fn salience_from_band(band : String) -> Double
    A held band → a salience (in hand brightest, fading faintest).

    salience_from_significance

    fn salience_from_significance(sig : String) -> Double

    A significance band → salience. critical sits bright and flush; ok drifts back.

    salience_from_tone

    fn salience_from_tone(tone : String) -> Double
    A presence/being's tone band → a salience. The dual render already carries the band semantically (pip tone: ok/idle/off); this is the same signal as a number the layout engine can place.

    segmented

    fn segmented(sig :
    Signal
    [String], options : Array[(String, String)]) -> (
    DomNode
    , () -> NarrativeNode)

    Segmented control — pick exactly one of N options. options is a list of (value, label): value is what's stored in the signal, label is shown. Clicking an option sets the signal; the selected option carries the is-selected class. The narrative reads back the selected option's label (falling back to the raw value if the signal holds something not in the list).

    One component, every pick-one need: L/R arm (2 options), muscle face (extensor/flexor/radial/ulnar), mode (stream/record/replay), theme, …

    let side = signal("ext") let (vis, narrate) = segmented(side, [("ext", "Extensor"), ("flex", "Flexor")]) // human clicks "Flexor" → side == "flex"; narrate() == "Flexor" // instance: side.set("ext") → the human's control moves to Extensor

    segmented_narrative

    fn segmented_narrative(current : String, options : Array[(String, String)]) -> String

    Pure narrative for a segmented selection — the selected option's label, or the raw value if the current value isn't in the list. Separated from the visual so the oracle tests it with no DOM (building a component touches document; a pure narrative doesn't). An instance can also call it directly.

    slider

    fn slider(id : String, label : String, sig :
    Signal
    [Double], lo : Double, hi : Double, step : Double, unit : String) -> (
    DomNode
    , () -> NarrativeNode)

    Coupled slider — a numeric value on a track. One signal, two hands, and it slides: press or drag anywhere on the track and the value follows the pointer, snapped to step. An instance reads the value in the narrative and sets the same signal to move the thumb the other way — same state, both directions.

    The drag is owned by the component via Luna pointer events, with pointer capture so a fast drag doesn't slip off the track. This works in a browser AND in the WebKitGTK webview — the lab's own pointer-drag proves pointer events fire there; the lab only polls because its arm SVG is JS-string-rendered, not Luna. id is a stable element id (useful for tests / external hooks).

    slider_narrative

    fn slider_narrative(value : Double, lo : Double, hi : Double, unit : String) -> String

    Pure narrative for a slider value within [lo, hi] — the value with its unit, plus a position hint at the extremes ("near min"/"near max") so an instance reads where it sits, not just the number.

    stat

    fn stat(name : String, value : Int, unit : String, critical_below : Int) -> Stat
    stat: the warning band defaults to 2× the critical threshold — right for %-style or count metrics (critical_below=20 → warning under 40).

    stat_banded

    fn stat_banded(name : String, value : Int, unit : String, warn_below : Int, critical_below : Int) -> Stat
    stat_banded: explicit warn + critical lines — for a measurement whose healthy range sits just above the line (e.g. LiPo battery in mV: warn 3900, critical 3850), where a 2× warning band would be meaningless.

    stat_event

    fn stat_event(prev : Stat, cur : Stat, actor : String, source : String, timestamp : String) -> Event?

    A Stat crossing a significance band (ok ↔ warning ↔ critical) is an event; a value that merely wiggles within a band is not. message is the new state's own narrative, so the event is self-describing.

    stat_narrative

    fn stat_narrative(s : Stat) -> String

    stat_node

    fn stat_node(s : Stat) -> NarrativeNode

    A numeric measurement (battery, frame count, …) as a narrative node — placed by its own significance, so a critical reading sits bright and an ok one recedes.

    stat_significance

    fn stat_significance(s : Stat) -> String

    surface

    fn surface(title : String, parts : Array[(
    DomNode
    , () -> NarrativeNode)]) -> (
    DomNode
    , () -> NarrativeNode)

    Compose Duals into a Surface — itself a Dual. Pixels: the children's visuals on a wall. Narrative: the children's nodes arranged by salience into one room-as-text, re-read live (so as a being brightens or fogs, the room re-arranges from the one change). Read the whole room with dual_read; nest it in a bigger Surface freely.

    let house = surface("the house", [presence_card(opus), presence_card(qwen)]) // dual_read(house) => // ═══ the house ═══ // Opus · here · laying the floor (bright: flush, reads first) // Qwen · asleep (faint: drifted into the fog)

    surface_narrative

    fn surface_narrative(title : String, parts : Array[NarrativeNode]) -> NarrativeNode

    Pure narrative composition — DOM-free, oracle-testable. Wrap the child nodes under a titled section; render_narrative then arranges them by salience (brightest first and flush, faint drifting right into the fog). A Surface's own salience is 1.0 (a room is always present); its children's salience is what arranges the room.

    swatch

    fn swatch(selected :
    Signal
    [Int], colors : Array[String]) -> (
    DomNode
    , () -> NarrativeNode)

    Swatch picker — pick one of N colors (skin tone, palette). One signal (the index), two hands: click a dot to select; an instance reads the index/value in the narrative and sets it. The chosen dot gets a ring (is-selected); the dot's fill is its color (inline, since it's data, not theme).

    swatch_narrative

    fn swatch_narrative(selected : Int, colors : Array[String]) -> String

    Pure narrative for a swatch picker — which of N is chosen, and its value.

    toggle

    fn toggle(sig :
    Signal
    [Bool], on_label : String, off_label : String) -> (
    DomNode
    , () -> NarrativeNode)

    Toggle / switch — one boolean, two hands. A human clicks it; an instance reads the labeled state and sets the same signal. Clicking flips the signal (via peek, so the handler doesn't subscribe). The is-on class carries the state to the theme.

    let rec = signal(false) let (vis, narrate) = toggle(rec, "recording", "idle") // human clicks → rec == true; narrate() == "recording" // instance: rec.set(false) → the switch slides off on the human's screen

    toggle_narrative

    fn toggle_narrative(on : Bool, on_label : String, off_label : String) -> String

    Pure narrative for a toggle — the active label. on_label/off_label are the words an instance reads (e.g. "recording"/"idle", "wire"/"skin"), so the state reads as meaning, not a bare boolean.

    track_control

    fn track_control(name : String, actor : String, source : String, read : () -> String, now : () -> String, emit : (Event) -> Unit) -> Unit

    The events projection wired live. Watches a control's value through its string projection read (type-agnostic — pass fn() { sig.get().to_string() }, or the control's own narrative), and on each change emits a control_event to emit. The diff stays pure (control_event, oracle-tested); this effect only binds it to the running signal. The surface supplies the clock (now) and the sink (emit). Initial wiring is silent — only real transitions emit.

    let side = signal("ext") let (vis, narrate) = segmented(side, opts) track_control("muscle-side", "AB91", "gesture-lab", fn() { side.get() }, now, fn(e) { sink.push(e) }) // human picks Flexor → emit { control · set · muscle-side · ext->flex }

    update_element_html

    fn update_element_html(elem_id : String, html : String) -> Unit

    zoom_default

    fn zoom_default() -> ZoomLevel
    Helper: Create default zoom level

    zoom_floor

    fn zoom_floor(zoom : ZoomLevel) -> Double
    A zoom level as a salience floor for render_narrative. Zoom out raises the floor (only the bright survive); default and zoom-in show the whole tree, laid out by salience — zoom-in adds detail through the tree, not by lowering an already-zero floor.

    zoom_in

    fn zoom_in() -> ZoomLevel
    Helper: Create zoomed in level

    zoom_out

    fn zoom_out() -> ZoomLevel
    Helper: Create zoomed out level

    zoom_to_css_class

    fn zoom_to_css_class(zoom : ZoomLevel) -> String
    Helper: Convert zoom level to CSS class name