widget

package
v0.2.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 3, 2026 License: Apache-2.0 Imports: 18 Imported by: 0

README

widget

The widget SDK: the contract a widget implements, the registry a host binary mounts them through, and a small Mermaid dialect for declaring one, with the interpreter that turns it into a typed, resolved IR a generator emits from. You write fourteen blocks in a fixed order; the interpreter resolves every reference to a handle, derives the three things an author must not be able to contradict, and reports every mistake with a line, a column, a class and a repair.

It is v0.1. The API makes no compatibility commitment yet: the IR records, the finding vocabulary, the widget contract and the entry points are all expected to move as the first real widgets land. dialect 0 is a hard version pin — an interpreter that implements version n refuses any other version rather than guessing which constructs it can still handle — so a document and an interpreter are never quietly mismatched even while both are moving.

The language is a Mermaid dialect for a measured reason rather than an aesthetic one: an invented DSL starts an agent below 20% accuracy on syntax unfamiliarity alone, and mermaid's training-data presence means a dialect pays tokens only for its custom vocabulary.


The SDK, in one page

A widget is a vertical slice — its own state, its own events, its own live region, its own UI — and every widget a host serves runs inside one process. IWidget[S, I] is the contract — S is the widget's own state type and I is the HOST's identity type, threaded through and never read — and its six methods are the lifecycle phases of docs/ontology.md rather than a shape chosen for convenience: Register, Mount, Reduce, Render, Unmount, and Snapshot, which a host reads without knowing the widget's type. Effects are returned as live.Effect values whose Run functions perform the work; there is no Effect method on the widget interface. S is the widget's own state type and every phase is written in it, so nothing an author writes ever holds an untyped state. Only the registry holds widgets of several different S at once, and widget.Register is where — once, behind a generic shell, in an unexported adapter — that is erased.

tick is the one phase with no method of its own, and its absence is the ontology's reading rather than an omission: a tick is what a Stream delivers without a user, so it reaches Reduce as the event the stream carries — the same way an effect failure does, so a reducer sees every cause in one switch.

A live UI session means one accepted gotth-live WebSocket connection and the server-side state maintained for that connection. In this README, "session" always means that connection's lifetime, not a login or a stored agent conversation. Each connection gets an initial state value for each mounted widget; its reducer and renderer run serially on the connection's owning goroutine. A reconnect creates a new session and initializes state again; restoring durable application data is the host's responsibility. Separate state values can still refer to shared objects: this is not deep copying or memory isolation. See the connection implementation and widget initialization.

Widget count does not determine connection count. A date widget and a widget with many asynchronous workers share the same WebSocket when mounted on the same live page. live.App.ActiveConnections() counts that connection once. The service that owns connection state (currently named Actor in internal/session/actor.go) serializes all of its widget reducers and renderers on one goroutine; effects do their asynchronous work on additional goroutines. Their count is a separate measurement. A consumer explicitly opening another transport creates another connection; widget mounting does not.

The host app is the runnable composition that owns the application process. gotth-live acts as an in-process service, managing live connections and their goroutines. That service can outlive many individual connections. Widget Mount/Unmount follow each connection's lifetime; shared application services retain their own owners and lifetimes. The current bounded cleanup behavior is described below.

registry := widget.NewRegistry()
widget.MustRegister(registry, nodestatus.NewNodeStatus())

config, err := registry.LiveConfig(widget.MountOptions{
    Origins:      []string{"http://127.0.0.1:8080"},
    Authenticate: live.Anonymous, // production replaces all three
    Authorize:    live.AllowAll,
    CSRF:         live.NoCSRFCheck,
})
app, err := live.New(config)

LiveConfig is the whole of "mounting a widget into a host": one gotth-live fragment per widget, the union of their browser-sendable event names, and a reducer that routes by region first and wire name second, broadcasting only what names neither — which is the library's own effect-failure and slow-client notices.

Events and Internal are the two sides of one line. A generated registration puts an event a declared stream delivers in Internal and everything else in Events, and LiveConfig registers only Events with the live library while routing both. Registration is the only thing that makes a wire name sendable by a browser, so an event with a server-side source is refused before any reducer runs — otherwise a browser could post the source's own truth and the widget could not tell the two apart.

Payloads names what each event carries. A generated widget emits one constant per declared payload field and repeats the same names as data on the registration, because the field names are a contract with whatever fills the event. Renaming a field in the document is then a compile error at every site that fills it, rather than a card that silently stops updating.

There is no per-widget port, container or connection. Widgets share the host's address space; the session serializes reducer work and effects can run concurrently. Mounting a widget does not allocate a private heap or require a dedicated goroutine per widget. examples/widget/ is the smallest host that does it.

Shared memory, ownership and cleanup

A widget is an in-process composition unit with per-session state and lifecycle hooks. Those hooks do not create a memory-isolation boundary. Its interface specifies operations, not an implementation strategy or automatic resource ownership. The registry reuses registered widget implementations across sessions; mutable receiver fields or injected dependencies can therefore be shared even when each session has its own state value.

Concern Consumer contract
State passed by value Copying a struct, slice, map or pointer does not recursively copy referenced data. Reducers must not mutate prior state; copy the parts that change or use immutable values.
Concurrent effects Return results through events instead of mutating reducer-owned state. Shared mutable dependencies need an explicit owner or synchronization protocol: channels, locks or appropriate atomics.
Read-only sharing Data initialized before publication and never mutated afterward may be shared without a mutex. Exclusive ownership transfer is another option, provided the sender stops using the transferred mutable data.
Borrowed resources Passing a resource does not transfer cleanup responsibility automatically. Its owner must keep it usable until all authorized users finish; a child must not close a borrowed pool or clear shared state on its own exit.
Shared reads and writes If one goroutine changes data while another accesses the same data, synchronize those accesses. When using a mutex, readers and writers must use the same mutex. Locking only the pointer handoff does not protect subsequent access to its contents.
Clearing a pointer thingy = nil assigns to the variable thingy; *thingy = nil writes to the location it points to, when the stored type permits nil. A shared location needs synchronization with readers in either case. Clearing one pointer slot does not clear other copies of the pointer or wait for users of the old object.
Garbage collection Ending a goroutine or unmounting a widget does not free everything it used. Ordinary Go objects remain alive while reachable. Unreachable memory becomes eligible for collection; neither immediate reclamation nor return to the OS is guaranteed.
External resources Files, sockets, subscriptions and foreign allocations require their documented cleanup. A reachable Go wrapper does not guarantee that its underlying resource is still open or valid.

For resources used by workers, the ownership protocol is: stop accepting new work, request cancellation, perform any documented action needed to unblock work, wait for affected workers to finish, then release resources they could still use and drop retained references. Cancellation is a request, not proof of completion. Clearing fields before users finish can introduce a race or a nil dereference; closing a resource too early can cause a logical failure even when its Close method is concurrency-safe.

Current shutdown boundary: gotth-live's Actor.shutdown cancels and joins every effect of the session — counting and logging one still running after EffectDrainTimeout, and continuing to wait — then calls teardown. The registry calls Unmount in reverse registration order, so no effect the library started outlives Unmount; an effect that ignores cancellation holds the session's shutdown open instead. Application-created goroutines are not automatically tracked. Keep resources used by outstanding workers valid until their users finish; the current SDK does not enforce that ownership protocol for arbitrary widget implementations.

These rules follow the Go memory model, garbage collector guide, and context.CancelFunc contract. Race-enabled tests can detect races on exercised paths; they do not prove correct shutdown ordering or the absence of all races.

Channel ownership discipline

Prefer a single owning goroutine and typed channel messages when coordinating mutable state, queued work or lifecycle transitions. A mutex is appropriate for a short synchronous operation on shared state. Immutable snapshots can be shared after safe publication. Choose the simplest protocol that makes the owner and completion visible; a widget does not need a dedicated goroutine just to implement this convention. This follows Go's mutex-or-channel guidance.

For an API that transfers ownership, document this agreement:

Stage Ownership rule
Before sending The sender owns the payload's mutable data and finishes any prior uses before handing it off.
Handoff A successful send transfers the payload ownership specified by the API. A buffered send means the channel has accepted the value, not that processing has finished. If a cancellation branch wins instead of sending, ownership stays with the sender.
After sending The sender must not read, mutate, clear, close or recycle the transferred mutable payload through any retained alias. The receiver may already be using it.
Receiving The receiver owns subsequent mutation and whatever cleanup responsibility the API explicitly transfers. Borrowed dependencies, such as a host-owned database pool, retain their original owner.
Returning ownership Use an explicit reply or other documented handoff before the original sender resumes access. A receipt acknowledging submission is not completion or return of ownership.
Sharing instead If both sides need concurrent access, declare immutable sharing or a shared synchronization protocol. Sending a pointer does not itself authorize concurrent mutation.

Go channel sends copy values. Sending a pointer leaves both pointer copies valid and pointing to the same object; sending a slice or map does not deep-copy its backing data. The ownership transfer is an API discipline, not pointer invalidation enforced by Go. Setting one variable to nil does not revoke other aliases. This resembles the intent of a move-based API, but C++ std::move itself is a cast; the receiving operation and type determine whether ownership actually moves.

Document the payload, owner, borrowed dependencies, completion signal and shutdown behavior at each asynchronous boundary. These are implementation and review obligations. The repository's GOROUTINE-SHARED-STATE advisory locates captured writes and later caller accesses without locally matched mutex sections or a recognized WaitGroup completion pattern. It does not follow arbitrary aliases or library lifecycle callbacks, and cannot prove that a channel or mutex protocol is correct.

A widget's region is a landmark. The generated root is an <aside> carrying aria-labelledby pointing at its own title's id, both spelled from the same exported <Widget>TitleID constant. Both halves are required rather than decorative: HTML-AAM maps a nameless <aside> inside sectioning content to generic, so a landmark with no accessible name is not a landmark, and a screen-reader user can neither jump to the widget nor skip it. role is not emitted — the element already carries it, and ARIA that restates HTML is one more thing that can disagree with it. Inside the chrome, the source line is emitted before the title, because that is the order it is read in: DOM order is reading order, and a host stylesheet that reversed it visually would be a reading order only sighted users get.

A browser event carries a region, and the region is not optional. The gotthlive.v1 Event frame requires fragment_id to be non-empty, at most 64 characters and to match ^[A-Za-z0-9_:.-]+$ (pkg/gotth/docs/protocol.md § 3.2), so an event sent without one is refused at the frame boundary and never reaches a reducer, a widget or the registry's routing. It is not a check this SDK performs and not one it can relax.

That string is the widget's own region directive — the identity W108 constrains to the same character set and the same 64 — so one spelling is checked at both ends of the wire, and a generated widget exports it as <Widget>Region for a caller to pass:

client.Send(nodestatus.NodeStatusEventHealth, nodestatus.NodeStatusRegion, fields)

This is also why a deployed region identity may not be renamed casually: every patch on the wire names it, and so does every event coming back.

One optional interface sits beside the contract. IDirtyDeclarer[S] lets a widget say which state changes its own region's markup depends on; a widget that does not implement it gets a whole-state comparison, which is always safe and never reports equal for two states that differ. A generated widget implements it from its document's computed dirty projection, so the declaration is derived from the same source the render is.

Generating a widget

What internal means in Go: only packages inside the directory tree rooted at the parent of internal may import its packages. Here, that parent is pkg/widget. Thus pkg/widget/internal/cmd/widgetc may import pkg/widget/internal/uigen: they are different packages, but both are inside the permitted tree. A package outside pkg/widget cannot import uigen, even from the same repository or Go module. This is an import boundary enforced by Go, not a process boundary or a restriction to a single package.

internal/uigen emits a .templ view and a Go scaffold implementing the contract, from one resolved document. gen.sh writes every generated widget in this checkout and, with --check, asserts the committed output is byte-identical to a fresh generation. From the export root, inside the toolchain container:

docker run --rm -v "$PWD:/workspace" -w /workspace \
    dis-gotth-live:latest bash pkg/widget/gen.sh          # or: gen.sh --check

Every exemplar that validates generates — the flagship raft card, the minimal status card and the relay pipeline — and gen.sh writes all three. The flagship is what makes the list a check rather than a formality: every block of the dialect appears in it, so a construct the generator stops emitting fails on the document that uses it. The relay pipeline was verified by hand during the P2 audit and asserted by nothing, which is the same thing as uncovered. Edges, channels, orbits, motion, legend, indicator and control all come out of it. What it still refuses is a control whose trigger is change, input or submit — a control declares no element kind, so those three have nothing to bind to and a binding on a button could never fire. uigen.Refusals() names the list, and Generate reports every construct it met rather than the first.

The interpreter's API

document, findings := widget.Interpret("card.widget", source)
if len(findings) > 0 {
    for _, finding := range findings {
        fmt.Print(finding) // card.widget:56:3: W401: … \n    fix: …
    }
    return // nothing generates before it validates
}

InterpretFile is the same call with the read done for you; its error is returned only when the file could not be read at all, which is a failure of its own rather than a clean run.

Three properties hold of every call:

  1. Both passes run to completion. The findings are every finding, never the first, sorted by (line, column, class) so two runs print byte-identical output. An author who fixes one error and re-runs to find the next learns that the tool tells them a fraction of the truth, and starts guessing ahead of it.
  2. Every finding is anchored, classified and repairable. It names its subject, states what is wrong in the present indicative, and ends with one imperative fix: naming the exact spelling to write.
  3. A document is sound only when no finding came with it. The IR is returned either way — recovery is total, so tooling can still show what parsed — but generating from an unsound document is generating from a guess.

The document, in twenty lines

widget NodeStatus
dialect 0
region "widget.node-status"
palette fieldStation

state
  field reachable type flag
end

bindings
  binding statusText
    when reachable then "reachable"
    otherwise "unreachable"
  end
end

labels
  label titleLabel text "Node status"
  label statusLabel binds statusText
end

…and on through fourteen blocks in one fixed order, which is also the resolution order. docs/examples/02-node-status.widget is the whole minimal document; docs/examples/01-cluster-heartbeats.widget is the widest one.

What the IR guarantees

Document is resolved (every reference is a handle, so a generator needs no error path for an unknown name), total (no field's absence means "work it out"), ordered (every collection is a sequence in declaration order, because a render must be byte-identical for equal state), anchored (every record carries a SourceSpan) and closed (nothing in it names a path, a host, an address or a credential).

Three records are computed rather than parsed — EdgeGeometry, Legend and DirtyProjection. They are in the IR so a generator need not re-derive them, and absent from the grammar so an author cannot contradict them.

What it does not do

The interpreter does not render or serve, and does not resolve a palette: a document names seven semantic tokens — surface, ink, muted, rule, accent, positive, warning — and the design system owns their values. It does refuse a palette directive naming a palette that does not exist (W208), because the set of names is closed even where the values are not somebody else's. It does not compile the host's connection status into a motion gate; Motion carries HostStatusGate so a generator reads that obligation rather than remembering it.

Resolving those seven names is the host's job and the SDK ships one palette for it: widget.PaletteByName takes the name a document declared — a generated widget exports it as a constant — and widget.Stylesheet(palette) returns that palette's values, the token classes that read them, the scene's structure, the motion gate and the reduced-motion rule. An unknown palette name is refused rather than defaulted, because a fallback renders a plausible-looking widget in the wrong colours.

The SDK does not open a stream. A Registration carries the streams a widget declared and the host resolves each source name against the data plane it has, because a widget document names no host, no address and no credential by construction — which is what makes one publishable.

Reading order

Document What it settles
1 docs/dialect.md The surface syntax: the fourteen blocks, the four Anka rules, the document-level IR
2 docs/examples/ Four commented documents, the last of which does not validate on purpose
3 docs/errors.md 70 error classes, each with its anchoring rule, message template and named fix
4 docs/ontology.md The 25 typed concepts the syntax is a surface for
5 docs/inventory.md The 135 concepts harvested from shipped code, with provenance
6 examples/widget/ A host that registers a generated widget and serves it

An agent authoring a widget for the first time should read docs/dialect.md and example 01, in that order, and reach for docs/errors.md only when the validator names a class.

Contributor tooling

internal/cmd/widgetc validates documents from a shell during development:

go run ./internal/cmd/widgetc validate docs/examples/*.widget
go run ./internal/cmd/widgetc generate -package nodestatus \
    -out examples/widget/nodestatus docs/examples/02-node-status.widget

It exits 0 clean, 1 on findings or a document that cannot be generated, and 2 when a document could not be read or the command line was wrong — an unrun check must never read as a pass. A script that reads the exit status must build the binary rather than go run it: go run reports a non-zero child status as its own exit 1, so the two lines above collapse into one and "I could not read your document" becomes indistinguishable from "your document is wrong".

generate refuses a document that reported anything and prints the findings rather than only the refusal, because nothing generates before it validates.

It is not a product surface: a consumer of this package calls Interpret and implements IWidget[S, I], and gen.sh is what runs the generator here.

Multiple instances of one definition

Use a typed keyed collection when a host snapshot contains many instances of the same generated widget:

cards, err := widget.NewKeyedCollection[CardState, Identity](
    "board.cards", generated.NewCardAt[Identity],
)
fragment := widget.KeyedFragment(cards, func(state BoardState) widget.KeyedState[CardState] {
    return state.Cards
})

The collection reuses actual generated widget methods, keeps registration fixed, and adds, removes or reorders members through immutable state. Card changes patch their own regions; structural changes patch the collection parent over the same socket. Check construction errors before mounting. The keyed collection guide includes concrete generated consumer snippets, the host subscription/cleanup contract, and links to its compiling WebSocket regression tests.

Documentation

Overview

Package widget is the widget SDK: the contract a widget implements, the registry a host binary mounts them through, and the interpreter for the small Mermaid dialect widgets are declared in.

Monolithic microservices

A widget is a vertical slice — its own state, its own events, its own live region, its own UI — and every widget a host serves runs inside one process. There is no per-widget port, container, deployment or connection. What separates two widgets is the same thing that separates two goroutines: the Go runtime schedules them across cores, a session's goroutine advances one widget's reducer at a time, and each effect gets a goroutine of its own at the actor boundary. The isolation a fleet of services buys with network calls, this buys with a routing table and a slice index.

That is the whole claim behind "monolithic microservices", and it is worth stating precisely because the parts of it that are true and the parts that are marketing are easy to run together. What holds: independent state, independent event namespaces, independent live regions, independent failure of an effect, and the scheduler's own parallelism across widgets. What does not: a widget cannot be deployed, restarted, scaled or rolled back on its own, because there is one binary. A panicking reducer is contained by the library's panic budget for that session and by nothing else.

Registry is where that cashes out. Registry.LiveConfig turns a set of registered widgets into one gotth-live configuration — one fragment per widget, the union of their event names, and a reducer that routes each event to the widget that owns it — which the host hands to live.New and serves.

The contract

IWidget is the widget contract, and its methods are the lifecycle phases of docs/ontology.md rather than a shape chosen for convenience: register, mount, event, render, effect, unmount, plus the type-independent state projection a host reads. This is what a generated widget implements and what P4's fleet is written against.

It is generic in the widget's own state type, and everything author-facing stays that way. A registry holds widgets of several state types in one ordered sequence, which is heterogeneity Go has no other way to express, so Register erases it — once, behind a generic shell, into an unexported adapter nothing outside this package can hold. That one function is the whole of where a widget's state type is forgotten and the whole of where it is asserted back.

The dialect

The contract the interpreter implements lives beside it in docs/ and is the authority on every question this comment does not settle: docs/dialect.md is the surface syntax, docs/ontology.md the typed concepts, docs/errors.md the error catalogue, and docs/examples/ four worked documents — three that validate and one that does not, whose annotations are this package's acceptance test.

The contract

Interpret parses and validates in one call and returns both an IR value and every finding. Three properties hold, and callers may rely on all three:

  • Both passes run to completion. The findings are every finding, never the first, sorted by (line, column, class) so two runs over one document produce byte-identical output. An author who fixes one error and re-runs to discover the next learns that the tool tells them a fraction of the truth, and starts guessing ahead of it.
  • Every finding is anchored, classified and repairable: it carries a file:line:column position, the identifier of a class in docs/errors.md, a message naming its subject in the present indicative, and one imperative fix naming the exact spelling to write.
  • Nothing generates before it validates. A returned document is sound only when no finding came with it; generating from an unsound document is generating from a guess.

What the IR guarantees

A Document is resolved, total, ordered, anchored and closed: every reference is a handle rather than a name, no field's absence means "work it out", every collection is a sequence in declaration order, every record carries a SourceSpan, and nothing in it names a file path, a host, an address or a credential. EdgeGeometry, Legend and DirtyProjection are computed rather than parsed — they are in the IR so a generator need not re-derive them, and absent from the grammar so an author cannot contradict them.

What this package does not do

It does not generate, render or serve. It does not read a palette: a document names seven semantic tokens and the design system owns their values. It does not compile the host's connection status into a motion gate — Motion carries HostStatusGate so that a generator reads the obligation rather than remembering it.

It also does not open a stream. A Registration carries the streams a widget declared, and the host resolves each source name against the data plane it has, because a widget document names no host, no address and no credential by construction — which is what makes one publishable.

Regenerating

gen.sh writes every generated widget in this checkout and, with --check, asserts the committed output is byte-identical to a fresh generation. The directive below is a discoverability anchor for `go generate`; the script is what CI runs, and it must be run inside the toolchain container that carries the pinned templ CLI.

Index

Constants

View Source
const (
	FieldFlag    = ir.FieldFlag
	FieldCounter = ir.FieldCounter
	FieldCount   = ir.FieldCount
	FieldText    = ir.FieldText
)

The four state field types.

View Source
const (
	TokenSurface  = ir.TokenSurface
	TokenInk      = ir.TokenInk
	TokenMuted    = ir.TokenMuted
	TokenRule     = ir.TokenRule
	TokenAccent   = ir.TokenAccent
	TokenPositive = ir.TokenPositive
	TokenWarning  = ir.TokenWarning
)

The seven semantic tokens. The namespace is closed: a widget writes a token name, never a value, so one document renders under any palette that maps the seven.

View Source
const (
	SlotTitle         = ir.SlotTitle
	SlotSource        = ir.SlotSource
	SlotDescription   = ir.SlotDescription
	SlotStat          = ir.SlotStat
	MarkerLarge       = ir.MarkerLarge
	MarkerSmall       = ir.MarkerSmall
	DirectionForward  = ir.DirectionForward
	DirectionReverse  = ir.DirectionReverse
	GuardWhen         = ir.GuardWhen
	GuardWhenNot      = ir.GuardWhenNot
	LabelLiteral      = ir.LabelLiteral
	LabelBound        = ir.LabelBound
	PredicateAtomic   = ir.PredicateAtomic
	PredicateComposed = ir.PredicateComposed
	ComparisonAtLeast = ir.ComparisonAtLeast
	ComparisonAtMost  = ir.ComparisonAtMost
	TriggerClick      = ir.TriggerClick
	TriggerChange     = ir.TriggerChange
	TriggerInput      = ir.TriggerInput
	TriggerSubmit     = ir.TriggerSubmit
	WriterEventField  = ir.WriterEventField
	WriterEventToggle = ir.WriterEventToggle
	WriterSignal      = ir.WriterSignal
	SignalSlowClient  = ir.SignalSlowClient
)

The four chrome slots, the two markers, the two channel directions, the two guard polarities, the two label sources, the two predicate forms, the two bound comparisons, the four triggers and the three writers.

View Source
const DialectVersion = validate.DialectVersion

DialectVersion is the one dialect version this interpreter implements. A document declaring any other version is refused rather than guessed at: a parser that recovers by assuming what was meant generates a widget the author did not write.

View Source
const (
	// FieldStationPalette is the palette name the shipped exemplars declare. It
	// is the interpreter's own constant, because the closed set of palette
	// names is what the validator refuses a document against (W208) and one
	// name spelled in two places is one that eventually differs.
	FieldStationPalette = ir.PaletteFieldStation
)

The one shipped palette, and the custom-property prefix every token resolves through.

A widget renders under any palette that maps the seven names; there is one here because one is what the exemplars name, and a second palette that no document referenced would be vocabulary nobody uses.

Variables

View Source
var (
	// ErrEmptyName is returned for a registration with no widget name.
	ErrEmptyName = errors.New("widget: a registration needs a name")
	// ErrInvalidRegion is returned for a region identity the wire cannot carry.
	ErrInvalidRegion = errors.New("widget: a region identity must match " + regionPattern)
	// ErrDuplicateName is returned when two widgets claim one name.
	ErrDuplicateName = errors.New("widget: two widgets claim one name")
	// ErrDuplicateRegion is returned when two widgets claim one live region.
	ErrDuplicateRegion = errors.New("widget: two widgets claim one live region")
	// ErrDuplicateEvent is returned when two widgets claim one wire name.
	ErrDuplicateEvent = errors.New("widget: two widgets claim one event wire name")
	// ErrEmptyEvent is returned for a wire name that is the empty string.
	ErrEmptyEvent = errors.New("widget: every event needs a wire name")
	// ErrUndeliveredStream is returned for a stream delivering an event the
	// widget did not declare, which is a subscription whose payload nothing
	// would route.
	ErrUndeliveredStream = errors.New("widget: a stream delivers an event the widget does not declare")
	// ErrUnknownPayload is returned for a payload describing an event the
	// widget does not declare, which is a set of field names nothing can fill.
	ErrUnknownPayload = errors.New("widget: a payload describes an event the widget does not declare")
	// ErrDuplicatePayload is returned when one event is described twice, which
	// leaves two answers to "what does this event carry".
	ErrDuplicatePayload = errors.New("widget: one event carries two payload declarations")
	// ErrEmptyField is returned for a payload field with no wire name.
	ErrEmptyField = errors.New("widget: every payload field needs a wire name")
	// ErrDuplicateField is returned when one event declares one field twice.
	ErrDuplicateField = errors.New("widget: one event declares one payload field twice")
	// ErrNoWidgets is returned when a host asks a registry holding nothing for
	// a configuration, which would serve a page with no live region on it.
	ErrNoWidgets = errors.New("widget: a host needs at least one registered widget")
)

The registration faults a host can commit. Every one of them is a startup mistake in a literal somebody wrote, which is why they are reported when the widget is registered rather than at the first connection.

Functions

func Interpret

func Interpret(documentName string, source []byte) (*Document, []Finding)

Interpret parses and validates one widget document and returns the resolved IR together with every finding, sorted by position. documentName is used verbatim in every finding's anchor — no absolute path is constructed and no working directory is printed, because a validator's output is pasted into issues, transcripts and pull requests.

The returned document is always non-nil and always safe to inspect; it is sound only when no finding is returned.

func InterpretFile

func InterpretFile(path string) (*Document, []Finding, error)

InterpretFile reads a document from disk and interprets it. The error is returned only when the file could not be read at all, which is a failure of its own: an unrun check must never read as a pass.

func KeyedFragment

func KeyedFragment[B any, S any, I live.IIdentity, W IWidget[S, I]](
	collection *KeyedCollection[S, I, W], project func(state B) KeyedState[S],
) live.Fragment[B]

KeyedFragment projects a collection into its enclosing gotth-live state. A host may override the returned Render and Dirty for columns or lane changes, while retaining Children for targeted card patches. Render must include all members.

func MustRegister

func MustRegister[S any, I live.IIdentity](registry *Registry[I], instance IWidget[S, I])

MustRegister is Register for a caller with nowhere to put the error: package initialisation and main, where the registration is a literal in the source and every fault Register reports is a mistake in that literal.

func ParseCount

func ParseCount(raw string, fallback int64) int64

ParseCount reads a count, keeping fallback when the value is not an integer.

A count is an ordinary number and is not monotonic: it goes down as readily as up, so unlike a counter it takes whatever the wire said.

func ParseCounter

func ParseCounter(raw string, fallback uint64) uint64

ParseCounter reads a counter, keeping fallback when the value is not a non-negative integer OR when it is lower than the value already held.

The second half is the type's own meaning rather than defensive coding: a counter is monotonic, so a delivery carrying a lower value is a delivery that arrived late, and applying it would walk the counter backwards and re-arm every animation gated on it. Out-of-order delivery repairs itself this way; a counter that accepted the older number would show the wrong total until the next message and would have looked right the whole time.

func ParseFlag

func ParseFlag(raw string, fallback bool) bool

ParseFlag reads a flag, keeping fallback when the value is not a boolean.

The spellings are strconv.ParseBool's, which is what an HTML form and every serializer this library speaks produce: "true"/"false", "1"/"0", "t"/"f", and their capitalised forms.

func Register

func Register[S any, I live.IIdentity](registry *Registry[I], instance IWidget[S, I]) error

Register adds one widget, reporting the first fault in its registration or the first collision with a widget already registered.

It is a function rather than a method because a method cannot take a type parameter, and the type parameter is the point: this is the generic shell CS-7 § 2 asks for, and [erasedWidget] is the unexported adapter behind it. Everything above the shell knows S; nothing below it ever needs to.

Failing here rather than at the first connection is deliberate: a duplicated region identity is a region that stops updating for reasons nothing explains, and a duplicated wire name is an event delivered to the wrong widget. Both are mistakes in a literal somebody wrote, and startup is where a mistake in a literal belongs.

func Stylesheet

func Stylesheet(palette Palette) string

Stylesheet is the whole of the CSS a generated widget's markup depends on: one palette's seven values as custom properties, followed by the token classes that read them, the scene's structure, the motion gate and the reduced-motion rule.

It is one string rather than two calls because a host serving the structure without the tokens, or the tokens without the structure, has a widget that renders wrongly in a way no error reports.

func TokenNames

func TokenNames() []string

TokenNames are the seven semantic token names, in palette order. It is the closed namespace a widget document writes into.

Types

type Binding

type Binding = ir.Binding

The IR. Every type below is documented on the record itself.

type BindingClause

type BindingClause = ir.BindingClause

The IR. Every type below is documented on the record itself.

type Channel

type Channel = ir.Channel

The IR. Every type below is documented on the record itself.

type Class

type Class = diag.Class

Class identifies a finding's error class in docs/errors.md.

type Comparison

type Comparison = ir.Comparison

The IR's closed sets.

type Control

type Control = ir.Control

The IR. Every type below is documented on the record itself.

type Direction

type Direction = ir.Direction

The IR's closed sets.

type DirtyProjection

type DirtyProjection = ir.DirtyProjection

The IR. Every type below is documented on the record itself.

type Document

type Document = ir.Document

The IR. Every type below is documented on the record itself.

type Edge

type Edge = ir.Edge

The IR. Every type below is documented on the record itself.

type EdgeGeometry

type EdgeGeometry = ir.EdgeGeometry

The IR. Every type below is documented on the record itself.

type Emphasis

type Emphasis = ir.Emphasis

The IR. Every type below is documented on the record itself.

type EventDeclaration

type EventDeclaration = ir.EventDeclaration

The IR. Every type below is documented on the record itself.

type EventField

type EventField = ir.EventField

The IR. Every type below is documented on the record itself.

type EventPayload

type EventPayload struct {
	// Event is the event's wire name, and must be one this registration
	// declares in Events or Internal.
	Event string

	// Fields are the event's wire field names, in the order the document
	// declares them. Declaration order rather than sorted, for the same reason
	// a Snapshot is ordered: two equal declarations compare equal
	// element-for-element.
	Fields []string
}

EventPayload is the wire field names one declared event carries.

type FieldType

type FieldType = ir.FieldType

The IR's closed sets.

type Finding

type Finding = diag.Finding

Finding is one location-anchored report from the error catalogue.

type GuardPolarity

type GuardPolarity = ir.GuardPolarity

The IR's closed sets.

type HostState

type HostState struct {
	// contains filtered or unexported fields
}

HostState is one session's state across every registered widget: one entry per widget, in registration order.

It is opaque on purpose, and it is the collection whose heterogeneity forces the erasure: one entry per widget, each of a type only its own widget knows. A host holds it, the library carries it between transitions, and the way to read one is Registry.Snapshots, which asks each widget for its own projection.

func (HostState) Len

func (state HostState) Len() int

Len returns how many widgets this state covers.

type IDirtyDeclarer

type IDirtyDeclarer[S any] interface {
	// Dirty reports whether a transition may have changed this widget's markup.
	// It is handed this widget's own state, before and after, never the host's.
	//
	// Over-declaring costs a suppressed render; under-declaring is a
	// correctness bug, and live/livetest.AssertDirtyComplete is what catches
	// it.
	Dirty(previous S, next S) bool
}

IDirtyDeclarer is a widget that declares which state changes its own region's markup depends on.

It is optional, and deliberately not part of IWidget: a widget that does not implement it gets the registry's whole-state comparison, which never reports equal for two states that differ and is therefore always safe. What the declaration buys is the other direction — a transition that moved state this region does not render is not a patch anybody needs — and what it costs if it is wrong is a region that stops updating, which is why the safe behaviour is the default and this is the opt-in.

A generated widget implements it from its document's computed dirty projection, so the declaration is derived from the same source the render is and cannot drift from it. A hand-written widget should implement it only when it can state which fields its markup reads; "all of them" is what the default already does.

type IWidget

type IWidget[S any, I live.IIdentity] interface {
	// Register declares the widget, once per process and before any session.
	// It must be pure and must return the same registration on every call.
	Register() Registration

	// Mount opens one session's copy of the widget and returns its initial
	// state together with any effects that start it. It is the first phase of a
	// session and happens exactly once.
	Mount(ctx context.Context, session live.Session[I]) (S, []live.Effect[I], error)

	// Reduce is the event phase: the pure transition from one state to the
	// next. Given equal state and an equal event it must return equal state and
	// equal effects, perform no I/O, read no clock, and mutate nothing it was
	// given.
	Reduce(state S, event live.Event) (S, []live.Effect[I])

	// Render draws the widget's live region. It must be a pure function of
	// state — equal state renders byte-identical markup — because that
	// comparison is what suppresses a patch nobody needs.
	Render(state S) templ.Component

	// Unmount releases whatever the session held. It is the last phase of a
	// session and happens exactly once, after which no other phase occurs.
	Unmount(ctx context.Context, session live.Session[I], state S)

	// Snapshot projects state into ordered name/value pairs, so a host, a test
	// or an operator can read a widget's state without knowing its type.
	Snapshot(state S) Snapshot
}

IWidget is one widget: a self-contained slice of live UI that a host registers once and mounts per session.

S is the widget's own state type, and it is a type parameter rather than an opaque one because nothing an author writes has any reason not to know it. A widget is written by whoever owns S; only the host holds widgets of several different S at once, and that is the host's problem rather than the author's. Register is where it is solved, exactly once and under a comment saying so.

The six methods are the lifecycle phases of docs/ontology.md, in the order a session drives them — register, mount, event, render, unmount — plus the one projection a host can read without knowing the widget's type. The set is closed by the ontology rather than by convenience: a widget cannot invent a phase, and a generator emits code for these and no others.

`effect` is the second phase with no method of its own, and it lost one on 2026-09-03 rather than never having had one. A live.Effect is a concrete struct carrying its own Run, so an effect a widget schedules already holds the closure that performs it — over whatever that widget owns — and a method the host called to hand the effect back had nothing left to decide. The phase is still the widget's: it is spelled in the effects Mount and Reduce return.

`tick` is the one phase with no method of its own, and its absence is the ontology's own reading rather than an omission. A tick is what a Stream delivers without a user, so it arrives at Reduce as the event the stream carries — the same way an effect failure arrives at Reduce as an event, so that a reducer sees every cause in one switch and every one of them is replayable.

Nothing here is called concurrently with itself for one session: a session is one goroutine, and Reduce and Render run on it. An effect's own Run is the only application code that may perform I/O, and the library runs it on a goroutine of its own.

func LookupWidget

func LookupWidget[S any, I live.IIdentity](registry *Registry[I], name string) (IWidget[S, I], bool)

LookupWidget returns the widget registered under a name, typed.

The caller supplies S, so unlike Register this assertion is not total: it reports false for a name nobody registered and for a widget whose state type is not the one asked for. That is deliberate — asking the wrong type is a question with no answer, and a package that panicked would be answering it.

type Indicator

type Indicator = ir.Indicator

The IR. Every type below is documented on the record itself.

type KeyedCollection

type KeyedCollection[S any, I live.IIdentity, W IWidget[S, I]] struct {
	// contains filtered or unexported fields
}

KeyedCollection reuses one generated IWidget definition for controlled, snapshot-driven instances. It has no mutable session state and owns no IO. Register, Reduce, Render and Snapshot are the actual widget methods; initial state comes from the host snapshot, so per-instance Mount/Unmount are not invoked. Use it for pure generated cards, not widgets owning mount resources. Host Init/effects/Teardown own subscriptions and their cancellation.

W preserves the concrete factory result. Neither authors nor this homogeneous collection erase S. Registry remains startup-only and is not modified here.

Validation and panics

Call State after loading or changing membership before passing a snapshot to this collection. NewKeyedState and KeyedState.Upsert validate keys without a collection's region prefix; State also checks the combined wire identifier. State returns errors for invalid input. Rendering or reducing a snapshot that bypassed that check panics if its identifiers cannot fit this collection. A factory that changes its registration after construction also panics: its Go implementation violated the fixed identity/event contract used by gotth-live.

func NewKeyedCollection

func NewKeyedCollection[S any, I live.IIdentity, W IWidget[S, I]](
	region string, factory func(region string) W,
) (*KeyedCollection[S, I, W], error)

NewKeyedCollection validates a definition once at startup. factory must be pure, bind the supplied region, and preserve the definition's registration.

func (*KeyedCollection[S, I, W]) Children

func (collection *KeyedCollection[S, I, W]) Children(state KeyedState[S]) []live.Fragment[KeyedState[S]]

Children declares independently dirty regions, without registering anything at runtime. Use Fragment or KeyedFragment to mount them in live.Config.

func (*KeyedCollection[S, I, W]) Events

func (collection *KeyedCollection[S, I, W]) Events() []string

Events returns the definition's browser-sendable event names. Internal stream events stay out of live.Config.Events, preserving default-deny ingress.

func (*KeyedCollection[S, I, W]) Fragment

func (collection *KeyedCollection[S, I, W]) Fragment() live.Fragment[KeyedState[S]]

Fragment mounts this definition once; sessions carry only KeyedState values.

func (*KeyedCollection[S, I, W]) Lookup

func (collection *KeyedCollection[S, I, W]) Lookup(state KeyedState[S], region string) (string, S, bool)

Lookup addresses only an exact member of the current snapshot. It rejects removed IDs even if their browser event was queued before the removal patch.

func (*KeyedCollection[S, I, W]) Reduce

func (collection *KeyedCollection[S, I, W]) Reduce(state KeyedState[S], event live.Event) (KeyedState[S], []live.Effect[I])

Reduce routes only declared events to the addressed live instance. The browser boundary additionally refuses Internal names before dispatch.

func (*KeyedCollection[S, I, W]) Region

func (collection *KeyedCollection[S, I, W]) Region(key string) (string, error)

Region derives the unique wire identity without lossy key normalization.

func (*KeyedCollection[S, I, W]) Render

func (collection *KeyedCollection[S, I, W]) Render(state KeyedState[S]) templ.Component

Render is the default parent, a div containing members in snapshot order.

func (*KeyedCollection[S, I, W]) RenderItem

func (collection *KeyedCollection[S, I, W]) RenderItem(state KeyedState[S], key string) templ.Component

RenderItem renders one generated widget at its assigned region. This lets a host compose columns or other structural markup in the parent fragment.

func (*KeyedCollection[S, I, W]) Snapshot

func (collection *KeyedCollection[S, I, W]) Snapshot(state KeyedState[S], key string) (Snapshot, bool)

Snapshot asks the addressed widget for its named state projection.

func (*KeyedCollection[S, I, W]) State

func (collection *KeyedCollection[S, I, W]) State(items []KeyedItem[S]) (KeyedState[S], error)

State validates a loaded snapshot against this collection's region budget.

type KeyedItem

type KeyedItem[S any] struct {
	Key   string
	State S
}

KeyedItem is one instance's stable key and controlled state. A key is local to its collection; all instances share one widget definition and event set.

type KeyedState

type KeyedState[S any] struct {
	// contains filtered or unexported fields
}

KeyedState is an immutable ordered snapshot. Its zero value is empty. S must also be treated immutably, exactly as live.Config requires.

func NewKeyedState

func NewKeyedState[S any](items []KeyedItem[S]) (KeyedState[S], error)

NewKeyedState copies items, rejecting duplicate or invalid keys. A collection also checks the complete region length through its State constructor.

func (KeyedState[S]) Get

func (state KeyedState[S]) Get(key string) (S, bool)

Get looks up one current member.

func (KeyedState[S]) Items

func (state KeyedState[S]) Items() []KeyedItem[S]

Items returns a copy in display order.

func (KeyedState[S]) Remove

func (state KeyedState[S]) Remove(key string) KeyedState[S]

Remove returns a new snapshot without key; a missing key leaves it unchanged.

func (KeyedState[S]) Reorder

func (state KeyedState[S]) Reorder(keys []string) (KeyedState[S], error)

Reorder accepts exactly one permutation of the current membership.

func (KeyedState[S]) Upsert

func (state KeyedState[S]) Upsert(key string, value S) (KeyedState[S], error)

Upsert replaces one member without moving it, or appends a new member.

type Label

type Label = ir.Label

The IR. Every type below is documented on the record itself.

type LabelSourceKind

type LabelSourceKind = ir.LabelSourceKind

The IR's closed sets.

type Legend

type Legend = ir.Legend

The IR. Every type below is documented on the record itself.

type LegendEntry

type LegendEntry = ir.LegendEntry

The IR. Every type below is documented on the record itself.

type Marker

type Marker = ir.Marker

The IR's closed sets.

type Motion

type Motion = ir.Motion

The IR. Every type below is documented on the record itself.

type MountOptions

type MountOptions[I live.IIdentity] struct {
	// Origins is the browser Origin allowlist, passed through unchanged.
	Origins []string

	// Authenticate, Authorize and CSRF are the live library's three security
	// hooks. Pass live.Anonymous, live.AllowAll and live.NoCSRFCheck to opt
	// out deliberately; a nil one is refused rather than defaulted.
	Authenticate func(request *http.Request) (I, error)
	Authorize    func(ctx context.Context, session live.Session[I], event live.Event) error
	CSRF         func(request *http.Request) error

	// Init schedules the host's own startup effects for a session, alongside
	// the effects each widget's Mount returned. It is where a host opens the
	// sources its widgets' declared streams name.
	//
	// There is no Execute beside it, and there was one until 2026-09-03. A
	// [live.Effect] carries its own Run, so a host effect is performed by the
	// closure the host built it with — over the source, broker or pool that
	// host owns — and the executor that used to type-switch an effect back to
	// its owner had nothing left to decide. What that executor guaranteed is
	// now the library's: an effect that names itself and carries no behaviour
	// fails deterministically rather than succeeding at nothing.
	Init func(ctx context.Context, session live.Session[I]) ([]live.Effect[I], error)

	// Logger and Dev are passed through to the live configuration.
	Logger *slog.Logger
	Dev    bool
}

MountOptions carries the decisions a registry cannot make for a host.

Everything here is either a security posture or a resource the host owns. None of it has a defensible default that a library could pick — an allowlist a library chose would be an allowlist nobody read — so the four hooks are passed straight through to the live configuration, which refuses a nil one.

type Node

type Node = ir.Node

The IR. Every type below is documented on the record itself.

type NumericBound

type NumericBound = ir.NumericBound

The IR. Every type below is documented on the record itself.

type Orbit

type Orbit = ir.Orbit

The IR. Every type below is documented on the record itself.

type Palette

type Palette struct {
	// contains filtered or unexported fields
}

Palette is one resolved mapping from the dialect's seven semantic token names to CSS values.

A widget document writes a token name and never a value, so this is where the name becomes a colour, and it is the host that holds one: colour policy belongs to the design system, and a widget that could mint a colour would be a widget that could leave the design system (docs/ontology.md, Palette).

The values are literals in this package's own source and there is no constructor that takes any, which is why nothing here validates one. A token value reaches a stylesheet as CSS rather than as text, so the moment a host can supply values, this type needs the validator that already exists for exactly that — reporting every failing entry rather than the first — and the honest move then is to centralize that one rather than to write a second.

func FieldStation

func FieldStation() Palette

FieldStation returns the shipped palette by name, for a host that knows which one it wants without asking.

func PaletteByName

func PaletteByName(name string) (Palette, bool)

PaletteByName resolves the name a document's `palette` directive wrote.

An unknown name is reported rather than defaulted: a fallback palette renders a plausible-looking widget in the wrong colours, which is the failure mode hardest to notice in review (docs/dialect.md § 8).

func (Palette) Name

func (palette Palette) Name() string

Name is the identifier a document's `palette` directive writes.

func (Palette) Value

func (palette Palette) Value(token Token) (string, bool)

Value returns one token's CSS value, and whether this palette maps it. A palette that maps all seven is what makes a document portable; one that maps six renders a widget with a hole in it, so the caller is told rather than handed an empty string.

type Placement

type Placement = ir.Placement

The IR. Every type below is documented on the record itself.

type Predicate

type Predicate = ir.Predicate

The IR. Every type below is documented on the record itself.

type PredicateKind

type PredicateKind = ir.PredicateKind

The IR's closed sets.

type Pulse

type Pulse = ir.Pulse

The IR. Every type below is documented on the record itself.

type Registration

type Registration struct {
	// Name identifies the widget within one host, and is the name its own
	// snapshots carry.
	Name string

	// Region is the widget's single server-owned live region, and the only
	// identity that crosses the wire: a patch names it, so changing it is a
	// client-visible change. It matches ^[A-Za-z0-9_:.-]{1,64}$.
	Region string

	// Events are the wire names a browser may send to this widget. The set is
	// exhaustive and default-deny: the host registers exactly these with the
	// live library, and a name absent here is refused before any reducer runs.
	Events []string

	// Internal are the wire names only this widget's own streams and effects
	// emit. They are deliberately not registered with the live library —
	// registration is what makes a name sendable by a browser — but the host
	// still needs them to route an emitted event back to the widget that
	// emitted it.
	//
	// A generated widget puts every stream-delivered event here, because the
	// subscription is what delivers it and a browser posting one of its own
	// would be forging the source's own truth.
	Internal []string

	// Streams are the subscriptions the widget declares and the host owns.
	Streams []StreamDeclaration

	// Payloads are the wire field names each declared event carries, in
	// declaration order, for the events that carry any.
	//
	// They are here because the field names are a contract between two
	// programs that never read each other's source: the widget takes them out
	// of an event, and whatever fills that event — a host adapter resolving a
	// declared stream, a browser control — puts them in. Until this existed the
	// filling half was a literal somebody typed, so renaming a field in the
	// document still compiled and silently stopped updating the widget.
	Payloads []EventPayload
}

Registration is everything a widget declares once per process, before any session exists.

It is the `register` phase as a value. The ontology's cardinality is what makes it a value rather than a call: registration happens exactly once per widget per process and cannot depend on a session, so a widget that could only describe itself while mounted could not be registered at all.

func (Registration) Payload

func (registration Registration) Payload(event string) ([]string, bool)

Payload returns the wire field names one declared event carries.

It is the read half of Registration.Payloads: a host filling an event, or a specification asserting that a host fills exactly the declared set, asks the registration rather than restating the names.

func (Registration) Validate

func (registration Registration) Validate() error

Validate reports the first fault in a registration, or nil.

It checks what one registration can be wrong about on its own. What only a set can be wrong about — two widgets claiming one name, one region or one wire name — is checked by [Registry.Register], because neither registration is at fault by itself.

type Registry

type Registry[I live.IIdentity] struct {
	// contains filtered or unexported fields
}

Registry is the set of widgets one host binary serves.

It is the `register` phase's home: a widget is registered once per process, before any session exists, and the registry is what turns that set into one gotth-live application. Registration order is preserved everywhere — fragment order, snapshot order, the order Mount and Unmount run in — because a host that rendered its widgets in map order would render two byte-different pages from one state.

A Registry is built at startup and read afterwards. Register is not safe to call concurrently with anything; everything else is read-only once the last widget is in.

The registry is not generic and cannot be. Its whole purpose is holding widgets of several different state types in one ordered sequence, which is the heterogeneity CS-7 § 2 is about: it is erased exactly once, in Register, into the unexported adapter below.

func NewRegistry

func NewRegistry[I live.IIdentity]() *Registry[I]

NewRegistry returns an empty registry.

func (*Registry[I]) List

func (registry *Registry[I]) List() []Registration

List returns every registration in registration order.

The slice is a copy, so a caller enumerating the host's widgets — a status page, a test, an operator command — cannot reorder what the host renders.

func (*Registry[I]) LiveConfig

func (registry *Registry[I]) LiveConfig(options MountOptions[I]) (live.Config[HostState, I], error)

LiveConfig turns the registry into one gotth-live configuration: one fragment per widget, the union of every widget's browser-sendable event names, and a reducer that routes each event to the widget that owns it.

Only Registration.Events is registered with the live library. Registration.Internal is deliberately left out and is still routed, because registration is the only thing that makes a name sendable by a browser: an event a declared stream delivers has a server-side source, and a browser posting one of its own would be forging that source's truth. Default-deny is what the library does with a name nobody registered, so leaving the name out is the whole of the enforcement.

This is the whole of "mounting a widget into a host". The host calls live.New on the result and serves the handler. Every widget on a connection shares its event loop and transport; effects may add goroutines. There is no per-widget process, port, connection or mandatory goroutine.

func (*Registry[I]) Lookup

func (registry *Registry[I]) Lookup(name string) (Registration, bool)

Lookup returns the registration filed under a name.

It returns the registration rather than the widget, and that is the one place the generic contract costs something: a name is a string, so a method that handed back an IWidget[S] would have to be told which S to assert to, which is a second erasure site for a question — "what does this widget declare" — that the registration already answers. LookupWidget is there for a caller that genuinely holds the widget's own type.

func (*Registry[I]) Snapshots

func (registry *Registry[I]) Snapshots(state HostState) []Snapshot

Snapshots projects every widget's state, in registration order.

It is what a test asserts on and what an operator reads: a host that could not describe its own widgets' state without knowing their types would have to be recompiled to answer the question.

type Related = diag.Related

Related is a finding's secondary anchor: the declaration a reference points at, or the first of two conflicting constructs.

type Role

type Role = ir.Role

The IR. Every type below is documented on the record itself.

type Scene

type Scene = ir.Scene

The IR. Every type below is documented on the record itself.

type Slot

type Slot = ir.Slot

The IR. Every type below is documented on the record itself.

type SlotKind

type SlotKind = ir.SlotKind

The IR's closed sets.

type Snapshot

type Snapshot struct {
	// Widget is the name of the widget the snapshot came from. A widget fills
	// it from its own registration, so a snapshot is self-describing once it
	// has left the widget that made it.
	Widget string
	// Fields are the state fields in declaration order.
	Fields []SnapshotField
}

Snapshot is a widget's whole state as ordered name/value pairs.

It is the only way a host, a test or an operator reads a widget's state without knowing its type, and it is ordered by state-field declaration order rather than by name so that two snapshots of equal state are equal element-for-element. A map would have made a snapshot's own text depend on iteration order, which is the same defect the IR forbids in a render.

type SnapshotField

type SnapshotField struct {
	// Name is the state field's declared name, as the document spells it.
	Name string
	// Value is the field's current value rendered as text: "true"/"false" for
	// a flag, base-10 digits for a counter or a count, the string itself for
	// text.
	Value string
}

SnapshotField is one field of a widget's state, rendered as text.

type SourcePosition

type SourcePosition = diag.SourcePosition

SourcePosition is one anchor. Columns are 1-based, in code points.

type SourceSpan

type SourceSpan = diag.SourceSpan

SourceSpan is a construct's extent, carried by every IR record.

type StateField

type StateField = ir.StateField

The IR. Every type below is documented on the record itself.

type Stream

type Stream = ir.Stream

The IR. Every type below is documented on the record itself.

type StreamDeclaration

type StreamDeclaration struct {
	// Name is the stream's declared name in the document.
	Name string
	// Source is the source name the document declared. It is a name the host
	// resolves, never an address.
	Source string
	// Delivers is the wire name of the event this stream delivers.
	Delivers string
}

StreamDeclaration is one long-running subscription a widget declares and the host owns.

It is a declaration and never a connection: it names a source and the event that source delivers, and nothing else. The widget cannot open it — a widget document names no host, no address and no credential by construction — so the host reads this and wires the source it has.

type TemplateSegment

type TemplateSegment = ir.TemplateSegment

The IR. Every type below is documented on the record itself.

type TextTemplate

type TextTemplate = ir.TextTemplate

The IR. Every type below is documented on the record itself.

type Token

type Token = ir.Token

The IR's closed sets.

type Trigger

type Trigger = ir.Trigger

The IR's closed sets.

type Writer

type Writer = ir.Writer

The IR. Every type below is documented on the record itself.

type WriterKind

type WriterKind = ir.WriterKind

The IR's closed sets.

Directories

Path Synopsis
internal
cmd/widgetc command
Command widgetc validates widget documents and prints their findings.
Command widgetc validates widget documents and prints their findings.
diag
Package diag carries the widget validator's location-anchored findings and the identifiers of the error catalogue every finding belongs to.
Package diag carries the widget validator's location-anchored findings and the identifiers of the error catalogue every finding belongs to.
ir
Package ir holds the widget interpreter's typed intermediate representation: one resolved, ordered, total record per document.
Package ir holds the widget interpreter's typed intermediate representation: one resolved, ordered, total record per document.
lex
Package lex turns widget source into positioned tokens, one slice per significant line.
Package lex turns widget source into positioned tokens, one slice per significant line.
mocks
Package mocks is a generated GoMock package.
Package mocks is a generated GoMock package.
parse
Package parse turns positioned tokens into a widget document's block structure, and reports every structural finding of the catalogue's W0 group.
Package parse turns positioned tokens into a widget document's block structure, and reports every structural finding of the catalogue's W0 group.
uigen
Package uigen turns one resolved widget document into the files that make it a running widget: a templ view and a Go scaffold implementing the SDK's widget contract.
Package uigen turns one resolved widget document into the files that make it a running widget: a templ view and a Go scaffold implementing the SDK's widget contract.
validate
Package validate resolves a parsed widget document into the typed IR and reports every finding of the error catalogue.
Package validate resolves a parsed widget document into the typed IR and reports every finding of the error catalogue.
refinement
v1
Package widgettest renders a registered widget's own live region, so a specification can assert on what a viewer receives rather than on how the markup was produced.
Package widgettest renders a registered widget's own live region, so a specification can assert on what a viewer receives rather than on how the markup was produced.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL