The one lifecycle for every operator-facing View.
A View has a CLOSED semantic document, a stable id, and the same open, patch
and close rail on every channel. Its :kind declares the capability policy:
:input is Human Input — a typed form that BLOCKS until a submit or
cancel action, timeout, or interruption;:live is a non-blocking picture driven by its producer and optionally ended
by an interrupt action.The distinction belongs in policy, not transport. Both kinds share the pending
registry, publish :view/open / :view/patch / :view/close envelopes, and
receive operator intent through action!. Renderers dispatch on :kind and
never infer behavior from an event name or resource path.
This namespace PARSES extension data against the CLOSED vocabulary declared by [[com.blockether.vis.contract.view]]. Input answers are coerced and checked once at the settle seam; live patches are normalized and materialized once before any surface sees them. Unknown keys are refused, while every declared key is preserved through normalization.
Secrets never travel as plaintext. A :password and an :otp field resolve to
an opaque vis-secret:<uuid> handle; plaintext stays in a process-local vault
and is readable only through reveal-secret from the trusted extension side.
The one lifecycle for every operator-facing View. A View has a CLOSED semantic document, a stable id, and the same `open`, `patch` and `close` rail on every channel. Its `:kind` declares the capability policy: - `:input` is Human Input — a typed form that BLOCKS until a `submit` or `cancel` action, timeout, or interruption; - `:live` is a non-blocking picture driven by its producer and optionally ended by an `interrupt` action. The distinction belongs in policy, not transport. Both kinds share the pending registry, publish `:view/open` / `:view/patch` / `:view/close` envelopes, and receive operator intent through [[action!]]. Renderers dispatch on `:kind` and never infer behavior from an event name or resource path. This namespace PARSES extension data against the CLOSED vocabulary declared by [[com.blockether.vis.contract.view]]. Input answers are coerced and checked once at the settle seam; live patches are normalized and materialized once before any surface sees them. Unknown keys are refused, while every declared key is preserved through normalization. Secrets never travel as plaintext. A `:password` and an `:otp` field resolve to an opaque `vis-secret:<uuid>` handle; plaintext stays in a process-local vault and is readable only through [[reveal-secret]] from the trusted extension side.
What a live view IS after patch N — and what the MODEL reads when it ends.
A form is answered once, so nobody has to own its state; a live view is a
stream of operations against nodes addressed by id, so somebody does.
apply-patch is that owner: pure, total, and the ONE place the surfaces
read from. The terminal pane, the companion screen and the model all paint
the same materialized map, so none of them can disagree about a row.
picture is the MODEL's surface: the finished view as DATA. Ids and tones
come with it, so a node the model read is a node it can patch, and a state is
never recovered from a sentence — the model acts on the same materialized map
the surfaces paint, budgeted, never on a description of it.
->markdown is the DOCUMENT that state renders into: what a human reopens,
what an artifact stores, what a transcript embeds. Markdown is a RENDERING,
never a fourth vocabulary: every node type has exactly one markdown form here,
so a view that reads well in the terminal already reads well on a page. Colour
is the only thing that cannot cross — where a surface paints red, the page
reads a [tone] token.
parse-markdown is that document read BACK. One markdown form per node type
in one direction is one form per node type in the other, so a whole view can be
AUTHORED as markdown and a rendered one re-read. What a budget left behind is
named, never guessed.
Nothing here evicts the RECORD. Bounds on keyed collections are REFUSALS
(spec/item-bounds) naming the bound and the node, a log's :window-lines
is only how much a surface holds hot, and the model's own budget always says
how many lines it left behind.
What a live view IS after patch N — and what the MODEL reads when it ends. A form is answered once, so nobody has to own its state; a live view is a stream of operations against nodes addressed by id, so somebody does. [[apply-patch]] is that owner: pure, total, and the ONE place the surfaces read from. The terminal pane, the companion screen and the model all paint the same materialized map, so none of them can disagree about a row. [[picture]] is the MODEL's surface: the finished view as DATA. Ids and tones come with it, so a node the model read is a node it can patch, and a state is never recovered from a sentence — the model acts on the same materialized map the surfaces paint, budgeted, never on a description of it. [[->markdown]] is the DOCUMENT that state renders into: what a human reopens, what an artifact stores, what a transcript embeds. Markdown is a RENDERING, never a fourth vocabulary: every node type has exactly one markdown form here, so a view that reads well in the terminal already reads well on a page. Colour is the only thing that cannot cross — where a surface paints red, the page reads a `[tone]` token. [[parse-markdown]] is that document read BACK. One markdown form per node type in one direction is one form per node type in the other, so a whole view can be AUTHORED as markdown and a rendered one re-read. What a budget left behind is named, never guessed. Nothing here evicts the RECORD. Bounds on keyed collections are REFUSALS (`spec/item-bounds`) naming the bound and the node, a log's `:window-lines` is only how much a surface holds hot, and the model's own budget always says how many lines it left behind.
The STORE OF RECORD of one live view: an append-only NDJSON file per view.
A live view is a STREAM, and nothing else in this process keeps one. The channel bus is a ring of 2000 events and the session journal is truncated per turn and past 16 MB, so a log that ran for twenty minutes would already be gone by the time the human scrolls back to its beginning. Here every line the engine ACCEPTED is on disk, in the order it accepted them.
One file, three kinds of line: the OPEN line is the view as it was declared, one PATCH line per accepted patch, and the CLOSE line is the verdict. A patch is appended BEFORE it is published, so a crash keeps everything the engine accepted rather than everything a surface managed to paint, and the file is opened in APPEND mode, so a resumed process never truncates a view it did not open.
Lines are wire JSON, which is what lets the file BE the artifact instead of a
re-encoded copy of one, and what lets a reader hand a patch line straight back
to view/normalize-patch: every live vocabulary is a closed table read in
either spelling.
The STORE OF RECORD of one live view: an append-only NDJSON file per view. A live view is a STREAM, and nothing else in this process keeps one. The channel bus is a ring of 2000 events and the session journal is truncated per turn and past 16 MB, so a log that ran for twenty minutes would already be gone by the time the human scrolls back to its beginning. Here every line the engine ACCEPTED is on disk, in the order it accepted them. One file, three kinds of line: the OPEN line is the view as it was declared, one PATCH line per accepted patch, and the CLOSE line is the verdict. A patch is appended BEFORE it is published, so a crash keeps everything the engine accepted rather than everything a surface managed to paint, and the file is opened in APPEND mode, so a resumed process never truncates a view it did not open. Lines are wire JSON, which is what lets the file BE the artifact instead of a re-encoded copy of one, and what lets a reader hand a patch line straight back to `view/normalize-patch`: every live vocabulary is a closed table read in either spelling.
Field validation for input View requests: a validator is a FUNCTION.
A field carries :validate — one function or a list of them. Each one is
called with the COERCED value, or with the value and the whole coerced
field id -> value map when it asks for a second argument, and answers:
nil or true — the value is fine.false — "is not valid", the message of last resort. Say something better.Validators run in the order they were written and the FIRST message wins, so
a field's :validate list reads like a checklist.
Validation is code, not data, and it never crosses the wire. The ENGINE is
the only authority: it runs every validator when a form is CONFIRMED
(com.blockether.vis.internal.view.core/submit!) and hands the surfaces
the errors it found, keyed by field. Neither the TUI band nor the companion
app owns a validation rule or re-checks anything as the operator types —
they show the engine's verdict, and drop a field's error the moment that
field is touched again, so nobody is scolded mid-word about a value they are
still typing.
A validator NEVER fires on a blank value. Emptiness is :is-required's
single job; a check that also rejected "" would quietly make every optional
field mandatory.
Field validation for input View requests: a validator is a FUNCTION. A field carries `:validate` — one function or a list of them. Each one is called with the COERCED value, or with the value and the whole coerced `field id -> value` map when it asks for a second argument, and answers: - `nil` or `true` — the value is fine. - a string — that string IS the field's error message. - `false` — "is not valid", the message of last resort. Say something better. - a throw — "could not be validated: …", because swallowing it would ACCEPT the value the validator was written to refuse. Validators run in the order they were written and the FIRST message wins, so a field's `:validate` list reads like a checklist. Validation is code, not data, and it never crosses the wire. The ENGINE is the only authority: it runs every validator when a form is CONFIRMED ([[com.blockether.vis.internal.view.core/submit!]]) and hands the surfaces the errors it found, keyed by field. Neither the TUI band nor the companion app owns a validation rule or re-checks anything as the operator types — they show the engine's verdict, and drop a field's error the moment that field is touched again, so nobody is scolded mid-word about a value they are still typing. A validator NEVER fires on a blank value. Emptiness is `:is-required`'s single job; a check that also rejected "" would quietly make every optional field mandatory.
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |