Vis answer IR — Hiccup-EDN, MDAST-equivalent, with strict canonical
form post-->ast.
Public surface: (->ast input) ; soft-normalize any input → [:ir & blocks] (render input flavor opts) ; one of :html :markdown :plain (extract-code ast) ; for vis --code (extract-text ast) ; for voice TTS (session->markdown db session-ref opts?)
Tags: ROOT :ir BLOCKS (11) :p :h{:level 1-6} :code{:lang} :ul :ol{:start} :li :quote :table :tr :th :td INLINES (11) :span{:preserve-ws? :nowrap?} :br :strong :em :c :a{:href} :img{:src :alt} :kbd :mark :sup :sub
Disclosure/collapsible answer blocks are intentionally unsupported:
no :details, no :summary, no HTML <details>/<summary> in answer IR.
─── Canonical form (invariant after ->ast) ───────────────────────────────
:ir children are exclusively block nodes.:span body (single string, no '\n').:code, :c, :kbd (single string, ws preserved).
Anywhere else, vector children only — no bare strings in the tree.[:br {}] nodes." \n continuation").:li children are either all blocks OR a single :p wrapping the
inline run.:ul/:ol children are exclusively :li.:table children are :tr; :tr children are :th/:td;
:th/:td children are inline.─── Coercion rules at the boundary ─────────────────────────────────────────
->ast is total and pure. Accepted inputs:
[:ir ...] — re-canonicalized (idempotent)
[:tag ...] (Hiccup, non-:ir) — wrapped in [:ir <node>]
"text" — wrapped in [:ir [:p [:span text]]]
sequential / vector of mixed — element-by-element coercion
anything else — surfaced as [:code {:lang "edn"} pr-str]
Vis answer IR — Hiccup-EDN, MDAST-equivalent, with strict canonical
form post-`->ast`.
Public surface:
(->ast input) ; soft-normalize any input → [:ir & blocks]
(render input flavor opts) ; one of :html :markdown :plain
(extract-code ast) ; for vis --code
(extract-text ast) ; for voice TTS
(session->markdown db session-ref opts?)
Tags:
ROOT :ir
BLOCKS (11) :p :h{:level 1-6} :code{:lang} :ul :ol{:start} :li
:quote :table :tr :th :td
INLINES (11) :span{:preserve-ws? :nowrap?} :br
:strong :em :c :a{:href}
:img{:src :alt} :kbd :mark :sup :sub
Disclosure/collapsible answer blocks are intentionally unsupported:
no `:details`, no `:summary`, no HTML `<details>/<summary>` in answer IR.
─── Canonical form (invariant after `->ast`) ───────────────────────────────
1. Every vector node has its attrs map at index 1 ({} when absent).
2. `:ir` children are exclusively block nodes.
3. Text lives ONLY in:
- `:span` body (single string, no '\n').
- raw bodies of `:code`, `:c`, `:kbd` (single string, ws preserved).
Anywhere else, vector children only — no bare strings in the tree.
4. Hard line breaks are explicit `[:br {}]` nodes.
5. Soft breaks (any '\n' inside a non-preserve-ws string) are collapsed
to a single space during canonicalization. This is the structural fix
for LLM output that emits cosmetic mid-paragraph indentation
(e.g. `" \n continuation"`).
6. `:li` children are either all blocks OR a single `:p` wrapping the
inline run.
7. `:ul`/`:ol` children are exclusively `:li`.
8. `:table` children are `:tr`; `:tr` children are `:th`/`:td`;
`:th`/`:td` children are inline.
─── Coercion rules at the boundary ─────────────────────────────────────────
`->ast` is total and pure. Accepted inputs:
[:ir ...] — re-canonicalized (idempotent)
[:tag ...] (Hiccup, non-:ir) — wrapped in [:ir <node>]
"text" — wrapped in [:ir [:p [:span text]]]
sequential / vector of mixed — element-by-element coercion
anything else — surfaced as [:code {:lang "edn"} pr-str]How a CommonMark SoftLineBreak (a bare between two non-blank
lines) is lifted into IR. CommonMark prose semantics collapse a soft
break to a single space, which is correct for model-authored answers
and thinking. User-typed / pasted input is line-oriented (an input
box, not a prose document): a literal newline is intent, so we lift
it to [:br] to preserve the pasted shape (code, line-numbered
dumps, tables). Bound to :hard via (markdown->ir text {:soft-break :hard}). Default :space keeps every existing caller unchanged.
How a CommonMark SoftLineBreak (a bare `
` between two non-blank
lines) is lifted into IR. CommonMark prose semantics collapse a soft
break to a single space, which is correct for model-authored answers
and thinking. User-typed / pasted input is line-oriented (an input
box, not a prose document): a literal newline is intent, so we lift
it to `[:br]` to preserve the pasted shape (code, line-numbered
dumps, tables). Bound to `:hard` via `(markdown->ir text {:soft-break
:hard})`. Default `:space` keeps every existing caller unchanged.(->ast v)Soft-normalize any answer-input value into canonical [:ir & blocks]. Pure, total, idempotent.
Identity-preserving: when the input already satisfies the canonical
invariants (canonical?), the return value is the SAME object.
This keeps downstream System/identityHashCode caches
(format-answer-with-thinking-data, etc.) hot across repeated
render passes — walker output is computed once per canonical IR
identity, not once per equal-but-fresh allocation.
Before canonicalization, Hiccup child positions are walked with
clojure+.walk semantics and non-vector sequential values (notably
lazy seqs from (map ...) inside answer IR) are safely realized to
at most 100 items, then replaced with an explicit … many more
marker when truncated. This avoids persisting Java LazySeq identity
strings and avoids hanging on infinite seqs.
See namespace docstring for the full canonical-form invariants.
Soft-normalize any answer-input value into canonical [:ir & blocks]. Pure, total, idempotent. Identity-preserving: when the input already satisfies the canonical invariants (`canonical?`), the return value is the SAME object. This keeps downstream `System/identityHashCode` caches (`format-answer-with-thinking-data`, etc.) hot across repeated render passes — walker output is computed once per canonical IR identity, not once per equal-but-fresh allocation. Before canonicalization, Hiccup child positions are walked with `clojure+.walk` semantics and non-vector sequential values (notably lazy seqs from `(map ...)` inside answer IR) are safely realized to at most 100 items, then replaced with an explicit `… many more` marker when truncated. This avoids persisting Java LazySeq identity strings and avoids hanging on infinite seqs. See namespace docstring for the full canonical-form invariants.
(answer->ir answer)Lift a final-answer value into canonical IR.
The Markdown-answer pipeline produces exactly two final-answer shapes:
{:answer markdown} - plain-prose answer{:vis/answer-mode :needs-input :answer/text string} - needs-input gateReturns canonical [:ir & blocks]. nil yields [:ir {}].
Anything outside the two canonical shapes is an upstream bug.
Lift a final-answer value into canonical IR.
The Markdown-answer pipeline produces exactly two final-answer shapes:
- `{:answer markdown}` - plain-prose answer
- `{:vis/answer-mode :needs-input :answer/text string}` - needs-input gate
Returns canonical `[:ir & blocks]`. nil yields `[:ir {}]`.
Anything outside the two canonical shapes is an upstream bug.(block-structurally-silent? form-source)True when the block source carries NO :code segment — it is purely
structural engine chrome (a bare title call). The engine stamps such a
block + its stream chunk with :vis/silent? true so channels drop the
whole entry from display (web folds it on :silent, the TUI hides the
form slot). Any block that DOES carry a :code segment flows through and
its raw source paints unconditionally.
True when the block source carries NO `:code` segment — it is purely structural engine chrome (a bare title call). The engine stamps such a block + its stream chunk with `:vis/silent? true` so channels drop the whole entry from display (web folds it on `:silent`, the TUI hides the form slot). Any block that DOES carry a `:code` segment flows through and its raw source paints unconditionally.
(canonical? x)Cheap structural check: x is already a canonical [:ir & blocks]
AST. When true, (->ast x) is the identity (returns the same
object), so downstream caches keyed on System/identityHashCode
hit cleanly across repeated render passes.
Cheap structural check: `x` is already a canonical `[:ir & blocks]` AST. When true, `(->ast x)` is the identity (returns the same object), so downstream caches keyed on `System/identityHashCode` hit cleanly across repeated render passes.
(diff-line-kind line)Classify ONE unified-diff line for channel-neutral colouring: :meta (file
headers ---/+++), :hunk (@@), :add (+), :del (-), or :ctx
(context / unchanged). The SINGLE classifier both channels share — the TUI maps
the kind to an ANSI colour, the web to a CSS class — so a diff fence colours
IDENTICALLY in both, from one source of truth (no per-channel copy to drift).
Classify ONE unified-diff line for channel-neutral colouring: `:meta` (file headers `---`/`+++`), `:hunk` (`@@`), `:add` (`+`), `:del` (`-`), or `:ctx` (context / unchanged). The SINGLE classifier both channels share — the TUI maps the kind to an ANSI colour, the web to a CSS class — so a `diff` fence colours IDENTICALLY in both, from one source of truth (no per-channel copy to drift).
(extract-code input)Walk the AST and return a vector of strings, one per [:code ...] block,
in source order. Used by vis --code.
Walk the AST and return a vector of strings, one per [:code ...] block, in source order. Used by `vis --code`.
(extract-text input)Walk the AST and return concatenated plain-text content of all [:p] blocks (inline content stripped). Used by voice TTS.
Walk the AST and return concatenated plain-text content of all [:p] blocks (inline content stripped). Used by voice TTS.
(ir? x)True when x is a canonical [:ir ...] AST.
True when x is a canonical [:ir ...] AST.
(markdown->ir text)(markdown->ir text {:keys [soft-break]})Parse a Markdown string into canonical answer-IR.
Idempotent: when the input is already canonical IR, returns it
unchanged (identical? preserved — cache-friendly).
This is the SINGLE entry point for turning Markdown source into IR. Used by:
Returns canonical [:ir & blocks] directly (no further ->ast
round-trip needed). Empty / nil input yields [:ir {}].
Implementation: commonmark-java parser + GFM tables / strikethrough
extensions, then a faithful Node→IR walker. Soft line breaks collapse
to a single space; hard line breaks become [:br].
opts (2-arity) currently understands {:soft-break :hard}, which
lifts every bare newline to [:br] instead of a space — used for
line-oriented user/pasted input so the rendered bubble keeps the
exact line structure the user typed. Default keeps prose semantics.
Parse a Markdown string into canonical answer-IR.
Idempotent: when the input is already canonical IR, returns it
unchanged (`identical?` preserved — cache-friendly).
This is the SINGLE entry point for turning Markdown source into IR.
Used by:
- the final-answer pipeline (model's plain-prose Markdown answer)
- thinking text from the model
- user-typed messages from the TUI input box
Returns canonical `[:ir & blocks]` directly (no further `->ast`
round-trip needed). Empty / nil input yields `[:ir {}]`.
Implementation: commonmark-java parser + GFM tables / strikethrough
extensions, then a faithful Node→IR walker. Soft line breaks collapse
to a single space; hard line breaks become `[:br]`.
`opts` (2-arity) currently understands `{:soft-break :hard}`, which
lifts every bare newline to `[:br]` instead of a space — used for
line-oriented user/pasted input so the rendered bubble keeps the
exact line structure the user typed. Default keeps prose semantics.(normalize-reasoning text)Canonical normalization for model reasoning / thinking text before it is
rendered as a trace. Reasoning streams carry whitespace-padded blank rows
(trailing spaces/tabs the model emits) and paragraph-style double newlines
that make the compact thinking block look ragged (glm-5.2 especially). Strip
per-line trailing whitespace, collapse every run of newlines down to ONE line
break, then give the trace BREATHING ROOM: a line that ENDS A SENTENCE
(./!/?/…, optionally closed by a quote/paren/bracket) and is
followed by more text gets a blank line after it, so consecutive sentences
read as separate paragraphs instead of a wall. Finally trim. Shared by every
channel so the TUI bubble and the web thinking card normalize identically.
Canonical normalization for model reasoning / thinking text before it is rendered as a trace. Reasoning streams carry whitespace-padded blank rows (trailing spaces/tabs the model emits) and paragraph-style double newlines that make the compact thinking block look ragged (glm-5.2 especially). Strip per-line trailing whitespace, collapse every run of newlines down to ONE line break, then give the trace BREATHING ROOM: a line that ENDS A SENTENCE (`.`/`!`/`?`/`…`, optionally closed by a quote/paren/bracket) and is followed by more text gets a blank line after it, so consecutive sentences read as separate paragraphs instead of a wall. Finally trim. Shared by every channel so the TUI bubble and the web thinking card normalize identically.
(parse-block-display form-source)Return the model's authored source as ONE verbatim :code segment.
The engine is full-Python: the source is Python and we keep it VERBATIM here —
exactly what the model wrote, no splitting, no classification. This is the
canonical segment (tests + the model's own context depend on it being the raw
bytes). Channels that want a beautified view call prettify-python at paint
time; the IR stays raw.
Pure helper. Never throws. Blank / nil input returns [].
Return the model's authored source as ONE verbatim `:code` segment. The engine is full-Python: the source is Python and we keep it VERBATIM here — exactly what the model wrote, no splitting, no classification. This is the canonical segment (tests + the model's own context depend on it being the raw bytes). Channels that want a beautified view call `prettify-python` at paint time; the IR stays raw. Pure helper. Never throws. Blank / nil input returns `[]`.
(prettify-python src)Beautify Python src via ruff (long calls/collections wrapped multiline,
black-style) for DISPLAY at a CHANNEL boundary. NOT applied to the canonical
:code IR (which stays VERBATIM — the executed/stored source, and the
model's exact bytes), so channels opt in when painting. Cached (bounded LRU)
and verbatim-safe: format-or returns src unchanged when ruff is
unavailable or the snippet isn't valid Python (partial streams, prose), so it
never changes meaning and never throws. ruff runs in-process via clj-ruff —
ONE process-wide cdylib, leak-free (confined arena + ruff_free_string per call).
Beautify Python `src` via ruff (long calls/collections wrapped multiline, black-style) for DISPLAY at a CHANNEL boundary. NOT applied to the canonical `:code` IR (which stays VERBATIM — the executed/stored source, and the model's exact bytes), so channels opt in when painting. Cached (bounded LRU) and verbatim-safe: `format-or` returns `src` unchanged when ruff is unavailable or the snippet isn't valid Python (partial streams, prose), so it never changes meaning and never throws. ruff runs in-process via clj-ruff — ONE process-wide cdylib, leak-free (confined arena + `ruff_free_string` per call).
(reasoning->ir text)Reasoning / thinking text -> canonical answer-IR. The SINGLE shared entry
point for rendering a model's thinking trace (TUI thinking bubble AND the web
thinking card), so both channels paint the SAME structure. Reasoning is
line-oriented (a trace, not flowing prose): normalize via normalize-reasoning
then lift every bare newline to a HARD break ([:br]) via {:soft-break :hard}. Without the hard break a bold heading collapses onto its body line
(the TUI **heading** body bug); with it the heading keeps its own line,
matching the web ticker's marked({:breaks true}).
Reasoning / thinking text -> canonical answer-IR. The SINGLE shared entry
point for rendering a model's thinking trace (TUI thinking bubble AND the web
thinking card), so both channels paint the SAME structure. Reasoning is
line-oriented (a trace, not flowing prose): normalize via `normalize-reasoning`
then lift every bare newline to a HARD break (`[:br]`) via `{:soft-break
:hard}`. Without the hard break a bold heading collapses onto its body line
(the TUI `**heading** body` bug); with it the heading keeps its own line,
matching the web ticker's `marked({:breaks true})`.Minimum number of HIDDEN reasoning rows required before a thinking trace is
COLLAPSED behind a disclosure at all. If the remainder beyond the preview is
smaller than this, the whole trace renders inline — a +1 more toggle that
only reveals one extra line is pure friction. One source of truth shared by
the TUI thinking bubble and the web thinking card.
Minimum number of HIDDEN reasoning rows required before a thinking trace is COLLAPSED behind a disclosure at all. If the remainder beyond the preview is smaller than this, the whole trace renders inline — a `+1 more` toggle that only reveals one extra line is pure friction. One source of truth shared by the TUI thinking bubble and the web thinking card.
Canonical reasoning PREVIEW height shared by every channel. Up to this many
rows/lines of a thinking trace stay visible; the remainder folds behind a
+N more disclosure (web) / ▸ THINKING +N more toggle (TUI). One
source of truth so the TUI bubble and the web card clamp reasoning to the
SAME height.
Canonical reasoning PREVIEW height shared by every channel. Up to this many rows/lines of a thinking trace stay visible; the remainder folds behind a `+N more` disclosure (web) / `▸ THINKING +N more` toggle (TUI). One source of truth so the TUI bubble and the web card clamp reasoning to the SAME height.
(render input flavor)(render input flavor opts)Render any answer input into a flavor.
Input: string | Hiccup vector | [:ir ...] AST | sequential of mixed Flavor: :html | :markdown | :plain Opts: {:context #{:answer :thinking :status :error} :max-length int - hard cap; truncate at paragraph boundary}
Render any answer input into a flavor.
Input: string | Hiccup vector | [:ir ...] AST | sequential of mixed
Flavor: :html | :markdown | :plain
Opts: {:context #{:answer :thinking :status :error}
:max-length int - hard cap; truncate at paragraph boundary}(search-text v)Universal plain-text projection for full-text search / clipboard /
logging. Accepts canonical IR, a markdown string, or anything
->ast can coerce; returns a single concatenated string suitable
for FT5 indexing or substring matching.
IR-side rendering: all prose/list/quote/table text collapses to spaces;
:code/:c bodies are included verbatim (often the highest-signal text
for search).
Strings are lifted via markdown->ir so search hits the same shape
regardless of upstream contract.
Idempotent on canonical input via the markdown->ir shortcut.
Universal plain-text projection for full-text search / clipboard / logging. Accepts canonical IR, a markdown string, or anything `->ast` can coerce; returns a single concatenated string suitable for FT5 indexing or substring matching. IR-side rendering: all prose/list/quote/table text collapses to spaces; `:code`/`:c` bodies are included verbatim (often the highest-signal text for search). Strings are lifted via `markdown->ir` so search hits the same shape regardless of upstream contract. Idempotent on canonical input via the `markdown->ir` shortcut.
(session->markdown db-info session-ref)(session->markdown db-info session-ref opts)Project a full session as a Markdown document on top of the IR pipeline.
Project a full session as a Markdown document on top of the IR pipeline.
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 |