Liking cljdoc? Tell your friends :D

com.blockether.vis.internal.view.core

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.
raw docstring

com.blockether.vis.internal.view.materializer

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.
raw docstring

com.blockether.vis.internal.view.sink

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.
raw docstring

com.blockether.vis.internal.view.validation

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.

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.
raw docstring

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close