open-standards/joule-ui/SPEC.md
Transaction Science 67d2efdafd joule-ui: the line language (SPEC §11), a streaming ingest into validated widget trees
joule-ui takes in OpenUI Lang 1.0 the way it takes in A2UI: a model writes a
surface as a program, one widget a line, and joule-ui-lang builds a widget tree
against the host's registry as the program streams. After every chunk a snapshot
holds the tree as far as it can be built, each widget checked by joule-ui's own
validate, and every error with its source line and reason code. State, queries,
operators, conditionals, built-in functions and sequences of actions are parsed
and refused not-declarative. A canonical writer turns a tree back into the
language; it parses back to the same tree.

- joule-ui-core: WidgetSchema::positional, the order a positional call passes
  props in (SPEC §3); absent from the wire form when undeclared.
- joule-ui-interaction: begin_interaction and Pending::finish, the lifecycle's
  three phases in two steps for a host whose handler is asynchronous (SPEC §6);
  run_interaction is built on them and seals what it sealed before.
- conformance/lang/v1: L1 statements, L2 ingest, L3 streaming, L4 the canonical
  form, L5 refusals, with an independent Python verifier written from §11.
- docs/create.md: the design of Create.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 18:09:10 -04:00

34 KiB
Raw Permalink Blame History

joule-ui — Specification

Standard joule-ui
Status Draft for Review
Version 0.3.0
Licence Specification text CC-BY-4.0 (LICENSE-SPEC); reference code Apache-2.0 (LICENSE)
Reference joule-ui-rs/

§1 What joule-ui is

joule-ui is a typed protocol for the moment a computation has to become something a human sees and acts on — and for making that moment accountable. It has two layers, and the second is the one no other UI protocol carries:

  1. The surface (§2–§4). A host publishes a registry of widgets it can render; an agent or program emits a typed widget tree against it. The host renders. No HTML over the wire, no code emission, no agent holding the renderer. This is the shape the field converged on (A2UI, MCP Apps SEP-1865, CopilotKit's declarative middle); joule-ui pins it as a protocol.

  2. The interaction (§5–§7). When a human fires an action on that surface, the invocation is an accountable event: authorized against a capability, metered in joules, and sealed into a tamper-evident receipt. A joule-ui session is therefore not a stream of opaque callbacks — it is a hash-chained ledger of what the user did, what it computed, and what it cost, verifiable by anyone holding the host's public key.

The surface layer makes joule-ui a peer of the declarative-UI protocols. The interaction layer makes it the only one you can hand to an auditor.

§2 The widget tree

A Widget is (name, props, children), recursive. name selects a widget in the host's registry; props is a map of typed values; children is an ordered list of Widgets. The wire form is plain JSON.

A PropValue is a closed set: Null, Bool, Number, Text, List, Object, and Action — an opaque action id (plus optional pure-data payload) that the host binds to a handler at render time. There are no closures, no expressions, and no code on the wire. An interaction (§5) is the firing of an Action.

§3 The registry

A Registry is the host's published catalog: for each widget name, a WidgetSchema declaring its prop slots (name → type, required/optional), the children it accepts (None / Any / a named allow-list), and whether unknown props are permitted. The registry is the host's contract; the renderer is the host's product surface and is out of scope for this standard.

A schema MAY declare its positional order (positional): the order in which a language that passes arguments by position, such as the line language of §11, maps a call's arguments to the widget's props, the name children standing for the child list. A schema that declares none has, as its order, its props in key order followed by children when it accepts children. The order is absent from the wire form when none is declared.

§4 Validation

validate(widget, registry) checks a tree against the registry and returns every error, not just the first: unknown widget, unknown/missing/mistyped prop, disallowed child. A conforming validator MUST be exhaustive and MUST be pure (no IO). A surface that validates is renderable by any conforming host of that registry; validation is the well-formedness attestation the surface layer provides.

§5 Energy and provenance

Every interaction carries a two-part energy figure in integer microjoules: measured_uj (present only for a hardware reading) and estimated_uj (always present), tagged with a Provenance — HwShunt (a hardware energy reading), ModelBased (a calibrated apportionment), or Estimator (a coarse estimate). This is the family's shared energy model; a figure is reported with the honesty of how it was obtained, never promoted.

Measurement itself is a recognized edge, not part of this standard: a Meter is any source of a cumulative microjoule counter, and it declares the provenance of its readings. A meter's figures are Estimator unless the meter declares otherwise: a meter opts in to ModelBased for a calibrated model, and to HwShunt only for readings of a live hardware counter (below). A meter MUST NOT declare HwShunt for figures that are not such readings — a deterministic stub, a model, a replayed counter. (The conformance pack's fixture meter, §8, is the one exception: it declares HwShunt only so the vectors exercise the reading path of the signed core, and like the pack's published steward key it attests nothing.) The sibling Sandbox standard's PowerMeter is the reference meter: it reads vendor energy counters (RAPL, NVML, Apple IOReport, and for work on its own process the per-process CPU energy the OS attributes to it) while they are live, apportions a reading to a process tree by CPU time (ModelBased), and falls back to a wall-clock estimate, labelled Estimator, where no counter is live. joule-ui defines what an interaction records; the host wires the meter it trusts.

Vendor energy counters. A reading from a vendor energy counter (RAPL, NVML, Apple IOReport, ...) MAY be graded as a hardware reading only while the counter is demonstrably live for the domain the figure claims: the implementation MUST have observed the counter covering that domain advance under load (for Apple IOReport: the CPU energy rail itself advancing in at least 5 sample intervals, in at least 4 of every 5, over at least 300 ms while the probing thread burns CPU). A counter that is present but not live — frozen, or republished in lumps — MUST NOT produce a hardware-graded figure; the implementation falls back to its estimator or reports the work unmetered, and SHOULD record why. A figure MUST NOT claim coverage of a domain its counter does not read; where the format has a coverage field, such a figure is Partial. joule-ui's energy record has no coverage field, so a figure from a counter that does not read the domain the work ran on — a GPU counter alone, for a handler that ran on the CPU — MUST NOT be graded HwShunt for the interaction: the reference adapters seal such a span as the meter's Estimator figure for its wall time.

§6 The interaction lifecycle

run_interaction is the protocol in one operation, in three phases:

  1. Authorize before running. The action is checked against a capability before its handler executes. A denied action's handler MUST NOT run. The capability check is a recognized edge: an Authorizer is any two-phase (authorize, charge) gate, and the sibling JCP standard's runtime — gating on a signed, joule-budgeted grant — is the reference one.
  2. Meter the handler. The handler runs once; the meter is sampled before and after; the difference is the interaction's energy, graded with the meter's declared provenance (§5). When the meter gives no reading — either sample is absent — the handler's wall-clock duration is charged at the meter's declared estimator power (20 W unless the meter declares its own), rounded up to whole microjoules, and the figure is Estimator. A handler that ran MUST NOT be recorded at 0 µJ for want of a reading: work that ran is not free, and a figure of zero would let a budget never bind on a host without a counter. A denied action's handler never ran, and its record carries 0 µJ Estimator.
  3. Charge the cost, with its provenance. The interaction's figure — a reading or an estimate — is debited from the capability's remaining budget, and the authorizer is handed the figure's provenance with it. So the budget binds on every host, settled after the fact against the energy the work cost as nearly as the host can know it; and an authorizer that keeps its own record of the debit MUST record it with that provenance, never an estimate as a reading.

A host whose handler cannot run inside one call (a browser's handler awaits the network) runs the same three phases in two steps: begin_interaction performs phase 1 and samples the meter, and gives a Pending only when the action is allowed; Pending::finish takes the handler's output and its wall-clock time on the host's own clock, and performs phases 2 and 3. A denied action gives no Pending, so no handler can run under it, and the denial is the Interaction phase 1 describes.

The result is an Interaction: the surface's content address (what the user was looking at), the action id, the content addresses of the inputs and the outputs (the genesis sentinel when denied), the verdict, the energy, the budget left, and the grant id. Inputs and outputs are content-addressed, not carried, so the receipt commits to what was supplied and produced without embedding it.

§7 The sealed, hash-chained interaction log

An InteractionLog seals each interaction as the next link of a chain. The canonical core (§7.1) is signed with the host's Ed25519 key; each link's prev is the content address of the previous link's core, and seq increments from zero. Allows and denies are both chained — a denial excised from the record is exactly the audit a malicious host would drop, so excising, reordering, or splicing any interaction MUST break the chain at that point. verify_chain checks every signature, the sequence, and every prev link, and reports the first break.

§7.1 The canonical core

The signed bytes are a fixed-order, length-prefixed concatenation: the profile tag joule-ui/interaction/2, then surface, action, actor, inputs, outputs, verdict (and deny reason, if any), provenance, estimated_uj, measured_uj (if present), budget_remaining_uj (if present), grant_id (if present), seq, and prev. Each field is preceded by its little-endian u64 byte length.

The actor is the principal that fired the interaction — the did:key:z… of a human or an agent (the standard makes no distinction; both are traced the same). It is mandatory: every sealed interaction names who fired it, and that name is bound into the signed core, so an interaction cannot be attributed to a different principal without breaking the chain. A host with no per-client authentication attributes fires to its own session principal (by convention its own did:key). The profile tag is …/interaction/2 precisely because the actor is now part of the preimage — a …/1 core (no actor) and a …/2 core are distinct signed bytes and never collide. Deterministic and dependency-light; the BLAKE3 of this core is the link's content address. (A host MAY additionally express a sealed interaction as a JCR-1 / in-toto+DSSE envelope for cross-family verification — a recognized edge, not required.)

§8 Conformance

  • C1 — surface validation. Given a registry and a widget tree, reproduce the exhaustive error list (empty for a valid tree).
  • C2 — interaction metering. Given a surface, action, actor, inputs, a stub meter, and a handler, reproduce the Interaction: verdict, energy, content addresses, and the sealed actor. The stub meter declares HwShunt as a fixture grade, so the recorded energy carries measured_uj (§5).
  • C3 — authorization. A denied action produces a deny Interaction whose outputs are the genesis sentinel and whose handler did not run; the actor is still named on the denial.
  • C4 — chain integrity. Seal a sequence; verify_chain accepts it and rejects any excision, reorder, or field mutation at the first broken link.
  • L1–L5 — the line language (§11). Statements, ingest, streaming, the canonical form and the refusals, from the pack in conformance/lang/v1/ (§11.9).

§9 Recognized edges

joule-ui is self-contained at its core and composes outward at named edges: the Sandbox standard supplies the reference Meter (a hardware reading while a vendor counter is live, its labelled estimate otherwise); JCP supplies the budgeted-grant Authorizer; JouleContract / in-toto+DSSE supply an optional cross-family receipt envelope; and any transport (MCP, REST, SSE, gRPC, Web MoQ) carries the validated surface and the interaction stream. None are required to use the protocol; each is the reference answer where a host needs that edge.

§10 Non-goals

A renderer (the host's), a transport (the consumer's), a component library (product surface), and code emission (an agent emits a typed tree against the registry, never a renderer). joule-ui pins the protocol and the receipt; the fast-moving surfaces stay the consumer's.

§11 The line language

A host MAY take in user interfaces written in the line-per-component language of the open-source OpenUI project, OpenUI Lang 1.0 (MIT licence; NOTICE): a program of statements name = Component(arg, ...), one a line, a name usable before the line that defines it. This section is how such a program becomes a joule-ui widget tree (§2) against a registry (§3), checked by the validator of §4, while the program is still arriving. The reference implementation is joule-ui-rs/crates/joule-ui-lang.

The language's grammar is parsed whole. What joule-ui does not carry (state, queries and mutations, operators, conditionals, member access, built-in functions, sequences of actions) is refused with the code not-declarative (§11.8): joule-ui puts no expression and no behaviour on the wire (§2), and the smallest piece that holds one is dropped.

A library is a registry and, optionally, the name of its root widget. Every widget name of a library starts with an uppercase ASCII letter and holds only ASCII letters, digits and _; none is Query, Mutation or Action; every name of a widget's positional order (§3) is one of its props, or children on a widget that accepts children, and none appears twice; the root names one of its widgets.

§11.1 Source text

The text is prepared in this order. Each line of the prepared text keeps the number of the source line it came from (1-based, counted in the text as received), and every line an error names (§11.8) is that source line.

  1. Every carriage return immediately followed by a line feed is removed. The text is then a sequence of lines separated by line feeds.
  2. Host marker lines. A line whose text starts with ]]>openui: is a marker line. When the text holds one, the program is only the lines that follow a marker line whose text after ]]>openui: starts with content, up to the next marker line; marker lines themselves and every other line are removed.
  3. Fences. A line whose first three characters are backticks (```) outside a fence opens a fence. When the text holds one, the program is only the lines inside fences, joined by line feeds; fence lines and all other lines are removed. Inside a fence a line whose first three characters are backticks closes it, unless a double-quoted string is open at its start: each line inside the fence is scanned for double quotes, a " toggling the string open or closed, and inside an open double-quoted string a backslash makes the next character not count (a line feed ends that effect). Single quotes do not count here. A fence still open at the end of the text runs to the end.
  4. Comments. Outside a string, // or # starts a comment that runs to the end of its line; the comment is removed and the line feed kept. A string opens at " or ' and closes at the next unescaped occurrence of the same quote character, across line feeds; inside a string a backslash takes the next character with it.
  5. Trimming. Space, tab, line feed and carriage return are removed from both ends of the text. Lines removed from the front leave the line numbering too.

§11.2 Lexing

The prepared text is read left to right into tokens. A space or a tab separates tokens; a line feed is a newline token. A character no rule below matches is skipped: lexing never fails.

  • Identifiers: [A-Za-z_][A-Za-z0-9_]*; true, false and null are keyword literals instead. $ followed by an identifier is a state name (the $ is part of it); @ followed by an identifier is an @ name. A $ or @ not followed by an identifier start is skipped.
  • Numbers: a digit, or a - followed by a digit when the previous token is not a value (a string, a number, usable or not, an identifier, a state name, a keyword literal, ) or ]; } is not one), starts a number: an optional -, digits, then a . followed by digits only when a digit follows the ., then, if an e or E follows, an exponent: the marker, an optional + or -, and digits. An exponent marker with no digit after it makes the token an unusable number. The number's value is the decimal value of its text, rounded to the nearest binary64.
  • Strings open at " or ' and run to the next occurrence of the same quote; a backslash takes the next character with it (a raw line feed is part of the string). A double-quoted string's content (between the quotes) is decoded as a JSON string (RFC 8259 §7); if it is not a valid one (an unknown escape, a raw control character, a \u escape that is short or names a lone surrogate) the content is taken exactly as written, backslashes included. In a single-quoted string, \n is a line feed, \t a tab, and a backslash before any other character is dropped and the character kept. A string still open at the end of the text is closed there: if its content then ends in an odd number of backslashes, one more backslash is added first; the token is marked open.
  • Punctuation, longest first: == != >= <= && ||, then = > < + - * / % ! ? : . , ( ) [ ] { }; a single & is && and a single | is ||.

§11.3 Statements

A statement starts at a token that is an identifier or a state name (its name) followed by =. Newline tokens before it are skipped; a line that does not start this way is skipped up to its next newline token, with no error. The statement's expression is the tokens after the =, newline tokens inside it left out, up to the newline token that ends it. A newline token ends it when all of these hold:

  1. the bracket depth is 0: each of ( [ { adds one, and each of ) ] } takes one away when the depth is above 0;
  2. no ternary is open: at depth 0 a ? opens one; a : at depth 0 moves the innermost open ternary to its second branch when it is still in its first (otherwise the : changes nothing); a value token at depth 0 (a string, a number or an unusable number, an identifier, a state name, a keyword literal, or a closing bracket that brings the depth to 0) gives the innermost open ternary's current branch an operand, and closes that ternary when the branch is its second; and
  3. the next token that is not a newline is not ?.

A statement that reaches the end of the tokens ends there.

A statement is complete when the host has ended the stream, or when its ending newline has arrived and a token that is not a newline follows it. Otherwise it is pending: only the last statement can be. A statement that reached the end of the tokens (rather than a newline) while its bracket depth was above 0 or its last token was an open string is open, pending or not.

A statement's kind: Query(...) or Mutation(...) as its whole expression makes it a query or a mutation; otherwise a state name makes it a state declaration; otherwise it is a value statement.

§11.4 Expressions

An open statement is first closed: a closing bracket that does not match the innermost open bracket is dropped, and the brackets still open are then closed, innermost first. The statement's tokens are then parsed whole by this grammar; tokens left after the expression are an error:

expression = ternary
ternary    = or [ "?" ternary ":" ternary ]
or         = and { "||" and }
and        = equality { "&&" equality }
equality   = comparison { ( "==" | "!=" ) comparison }
comparison = additive { ( ">" | "<" | ">=" | "<=" ) additive }
additive   = multiplicative { ( "+" | "-" ) multiplicative }
multiplicative = unary { ( "*" | "/" | "%" ) unary }
unary      = { "!" | "-" } postfix
postfix    = primary { "." key | "[" expression "]" }
primary    = string | number | "true" | "false" | "null"
           | "[" items "]" | "{" entries "}" | "(" expression ")"
           | ComponentName "(" items ")" | AtName "(" items ")"
           | StateName "=" expression | StateName | identifier
items      = { "," } [ expression { { "," } expression } { "," } ]
entries    = { "," } [ key ":" expression { { "," } key ":" expression } { "," } ]

A ComponentName is an identifier with an uppercase first letter followed by (; any other identifier is a reference. An unusable number is an error wherever it stands. A key is the next token, whatever it is: an identifier or a string gives its text, a number its text as written, a state name its text without $, a keyword literal its word, an @ name its text with @; any other token is the key ?. Commas between items and entries are optional, and any number of them is ignored.

A complete statement that does not parse is refused syntax (§11.8) at the source line of the token where parsing stopped (the last token when the tokens ran out, the statement's own line when it has none), and binds nothing. A pending statement that does not parse is not produced, with no error.

§11.5 Into a joule-ui tree

Bindings. Statements are taken in order; each produced statement binds its name, replacing an earlier binding, except that an open pending statement does not replace a complete one.

Statement refusals. A complete state declaration, query or mutation is refused not-declarative at its line. A reference to one evaluates to refused (below), with no further error.

The entry is the binding of root. With no root, it is the first value statement (by position in the text, among the bindings) whose expression is a call of the library's root widget.

Evaluation gives each expression a value: data (a joule-ui PropValue), null, a list or an object of values, a widget, an action, unresolved, held, or refused. Errors go to the statement whose text holds the expression.

  • A string, number or keyword gives Text, Number, Bool, or null.
  • A reference to a name with no binding, or to a statement being evaluated already (a cycle), is unresolved, and the name is listed as unresolved. Otherwise it is the value of the bound statement's expression, evaluated where it is used.
  • A list or an object gives a list or an object of its items' values.
  • A state name, an operator, a conditional, member access, indexing and an assignment are refused not-declarative at the line where that expression starts (a binary operation and a conditional start at their left operand, member access and indexing at their base, a call at its name).
  • Query(...) or Mutation(...) inside an expression is refused inline-reserved, at its line, its arguments not evaluated.
  • @Name(...) with a built-in function's name (Count, First, Last, Sum, Avg, Min, Max, Filter, Sort, Round, Abs, Floor, Ceil, Each) or a state action's (Set, Reset, Run) is refused not-declarative, its arguments not evaluated.
  • Action([x]) or @Action([x]) with a list of exactly one item is that item's value when it is an action (held when it is unresolved or held, refused when refused, and otherwise refused type-mismatch at the item's line); any other argument list is refused not-declarative at the call's line, unevaluated.
  • Any other @Name(args) is the action Action { id: Name, payload }. Every argument is evaluated and placed as data (below, null kept); if one was refused the action is refused, else if one is not yet written the action is held. The payload is absent for no argument, the argument for one, and the list of them for two or more.
  • A component call is built into a widget, below.

Placing as data (a prop other than children, an item of a list or object placed as data, an action's argument): data and actions are themselves; null is Null inside a list, an object or an action's arguments; a list or an object becomes List or Object of its items placed as data, an item that is not yet written or refused being left out; a widget is refused type-mismatch (at the line where the call's or the action's argument holding it starts); unresolved and held are not yet written; refused stays refused.

Building a widget. A call of a name the registry lacks is refused unknown-component at its line, its arguments not evaluated. Otherwise its arguments map, in order, to the names of the widget's positional order (§3); arguments past its end are refused together, excess-args, at the first extra one's line, and dropped unevaluated. Every other argument is evaluated, in order; an error about an argument is at the line where that argument starts.

  • The argument mapped to children must give a list. Its widgets become the widget's children in order; unresolved, held, null and refused items are left out; any other item is refused type-mismatch and left out. A widget instead of a list, or data, is refused type-mismatch and gives no children; null, unresolved, held and refused give no children.
  • Any other argument is placed as data. Null leaves the prop absent ("null"); a value not yet written leaves it absent ("not yet"); a refused one, or a widget (refused type-mismatch), leaves it absent ("refused").

The widget is then checked by validate (§4). While the stream is open, a widget that validate finds missing a required prop that is absent "not yet" is held, with no error. Otherwise every error at the widget itself is resolved as below and reported; then, if one of them drops the widget, it is refused; else the removals are made and the widget is checked again, until it passes:

validate error Resolution
a required prop missing absent "not yet" (the stream has ended): missing-required. Absent "null": null-required. Absent "refused": no further error. Otherwise missing-required. Each drops the widget.
a prop of the wrong type type-mismatch; a required prop drops the widget, an optional one is removed
a prop the widget does not declare type-mismatch; the prop is removed
a child the widget does not accept disallowed-child, at the child's own line and statement; the child is removed

An error the table names is reported at the line of the widget's call, unless it says otherwise. A widget that passes is the value, its children already built and checked.

The tree is the entry's value when that is a widget, and there is none otherwise. Each widget of the tree has a key: a widget that is a statement's value takes the statement's name, name#2 for the second use of that statement in the evaluation and so on; any other widget takes its parent value's key followed by / and its position (root/0; {key}/{slot} for a widget in a prop's slot). The nodes list each widget of the tree in pre-order with its key, the source line of its call, and its statement.

Orphans are the value statements the evaluation of the entry did not reach (none when there is no entry). The unresolved names and the orphans are reported sorted.

At the end of the stream: with no entry, the error is parse-failed (line 0, no statement) when no statement was produced and the prepared text is not empty, and no-root (line 0, no statement) otherwise; with an entry that gives no widget, no-root at the entry's line; with an entry found by the root widget rather than root, no-root as a warning at its line, and the tree renders.

Errors are reported sorted by line, then statement, then code (as text), then widget, then prop (an absent widget or prop before any), then warning (not one before one), and of the errors equal in all of these only the first is kept. While the stream is open, errors of a statement whose binding is pending are withheld.

A snapshot MUST NOT hold a widget its registry's validate refuses: the tree as a whole passes validate.

§11.6 Streaming

A host passes the text a chunk at a time; after each chunk, and when the host ends the stream, the ingest gives a snapshot: the tree, its nodes, the errors, the unresolved names, the orphans, and whether the text ended inside an open string or bracket (incomplete: the last statement is open). A chunk after the end starts a new stream.

  1. Each snapshot is that of §11.1–§11.5 for all the text so far, statements completing as §11.3 says. A complete statement's text no longer changes, so its value only changes as the statements it refers to arrive.
  2. The pending statement is closed for the moment (§11.2 strings, §11.4 brackets) and evaluated; its errors are withheld until it completes.
  3. An open pending statement never replaces a complete one of the same name (§11.5).
  4. Text that changes what came before it (a fence line arriving after unfenced lines) changes the preparation, and so every later snapshot.
  5. Streaming equals parsing. The snapshot at the end of a stream is the snapshot of the whole text taken at once, however it was cut.

§11.7 The canonical form

A tree that passes validate is written one statement a widget, in pre-order, each on its own line ending with a line feed: the root is root; every other widget is named by its widget name with its first letter in lowercase, followed by a count of that lowercase name from 1, in pre-order (card1, card2). A statement is name = Widget(args), the arguments joined by , : one for each name of the widget's positional order, children written [name1, name2] when the widget has children; a missing prop or empty children is null, and the trailing ones are left off. Values:

  • text as a JSON string: " and \ escaped, \b \f \n \r \t for those, \u00xx (lowercase) for the other characters below U+0020, everything else as itself;
  • a number as the shortest decimal that reads back to the same binary64 value, in positional notation, with no exponent and no fraction for an integer (-0 for negative zero); a number that is not finite cannot be written;
  • true, false, null, and lists as [a, b];
  • an object as {key: value} in key order (, between entries), a key written bare when it is an identifier other than a keyword literal, and as a JSON string otherwise;
  • an action as @Id(...): nothing inside for no payload, the items of a list payload of two or more, and the payload as the single argument otherwise. The id must be an identifier other than Action, a built-in function's or a state action's name.

A tree holding a widget whose name a call cannot take, a prop outside its widget's positional order, a prop whose value is null (a null argument is an absent one), or an action id the form cannot carry, is not written. The canonical form of a tree parses back (§11.5) to the same tree, and writing that tree again gives the same text.

§11.8 Reason codes

Code Raised when What is dropped
unknown-component a call names a widget the registry lacks the widget
missing-required a required prop is absent (once the stream has ended, for one not yet written) the widget
null-required a required prop is written null the widget
type-mismatch a value is not of its prop's type; a widget stands where data goes, or data among children; children not written as a list an optional prop or an item; a required prop drops the widget
excess-args more arguments than the widget's positional order the extra arguments
disallowed-child a child the parent's AllowedChildren does not accept the child
not-declarative state, a query, a mutation, an operator, a conditional, member access, indexing, an assignment, a built-in function, a state action, or a sequence of actions the statement, or the value
inline-reserved Query or Mutation inside an expression the value
syntax a complete statement that does not parse the statement
no-root at the end of the stream, no entry or an entry that is not a widget; a warning when the entry is found by the root widget everything (not for the warning)
parse-failed at the end of the stream, text with no statement everything

An error names its source line (0 for the program as a whole), its statement (empty for the whole program), its code, the widget and the prop it concerns where there is one, and a message for a person or a model to act on. Messages are not normative.

The widget an error names: for unknown-component, missing-required, null-required, excess-args, and a type-mismatch that validate raised or that is about a call's own argument (a widget in a prop's slot, the children argument or an item of it), the called widget; for disallowed-child, the child; for a type-mismatch about a widget placed as data inside a list, an object or an action's arguments, that widget. No other error names a widget. The prop an error names: for missing-required, null-required, and a type-mismatch that names the called widget, the prop (children for the children argument); no other error names one.

§11.9 Conformance

The pack in conformance/lang/v1/ holds the library it is written against (library.json, each widget's positional order written out) and five classes; a conforming ingest reproduces every case's expected value, errors compared without their messages:

  • L1, statements (l1-statements.json): the statements of each text at the end of the stream, in order (name, source line, kind, complete): each produced statement with its kind (value, state, query, mutation), and each complete statement that did not parse with the kind syntax; and whether it ended open.
  • L2, ingest (l2-ingest.json): each text's snapshot at the end of the stream.
  • L3, streaming (l3-stream.json): each case's snapshot after every chunk and at the end, which equals L2's snapshot of the joined chunks.
  • L4, the canonical form (l4-write.json): each tree's text, which parses back to the tree.
  • L5, refusals (l5-refusals.json): negative controls, each refused with its code at its line, and its whole snapshot.

MANIFEST.json names each file by its SHA-256. The independent verifier conformance/lang_verify.py is written from this section alone and shares no code with the reference implementation.


joule-ui v0.3.0 — the surface layer (v0.1), the metered, sealed interaction layer that makes a UI accountable by construction (v0.2), and the line language a model writes a surface in, taken in as it streams (v0.3).