Compilation Visualization
Note
Status: implemented. DialogueDown.Visualization compiles a script once and shows every
stage's output — plus the configuration it applied and the playbook it ships as — as one
interactive, self-contained HTML report.
Maturity. Unlike the compiler, the visualization tooling was built quickly ("vibe-coded") with
lighter design review. It is well tested and works, but its abstractions may still change. This
caveat covers every note under visualization/.
Table of contents
- Goal and scope
- Ubiquitous language
- The tabs
- Architecture
- Key design decisions
- Unavailable stages
- Stage tooltips
- Error and boundary cases
- Testability
Goal and scope
Full-process transparency is a goal of DialogueDown: a reader should be able to see what the compiler produced at each step, and trace any node back to the text it came from. The report renders each intermediate representation as a readable, interactive graph or table, from the Markdown AST to the playbook a runtime loads.
Out of scope here: the ddown visualize command and the served session, which wrap this
component and have their own notes under the reading guide.
Ubiquitous language
| Term | Meaning |
|---|---|
| Stage | A compiler step that produces an IR, shown as one tab. |
| IR | The tree or graph a stage produces (for example MarkdownDocument, DialogueGraph). |
| Node projection | INodeProjection<TNode>: a stage's Title, Description, and, per node, Describe (label, attributes, source, category) and Neighbors. One small implementation per IR family. |
| Walk | GraphWalk: the traversal that builds a display graph from an IR root and a projection, using a visited set so cycles terminate. |
| Display graph | DisplayGraph: one stage's title, description, nodes, and edges, plus optional tables and an unavailable reason. |
| Display node / edge | DisplayNode (id, label, attributes, source, span, category) and DisplayEdge (FromId, ToId, Kind, plus an optional Category and Label). |
| Category | A stable, cross-stage group name (call, speech, …) the client maps to a color. |
| Renderer | IDisplayRenderer: turns a display graph into one format (HTML or DOT). |
| Report | The multi-tab HTML page. |
The tabs
The tab order is the pipeline order, from configuration to shipped artifact:
| Tab | Shows | Built by | Note |
|---|---|---|---|
| Config | The applied dialogue.toml and its speakers |
ConfigurationProjection |
Configuration Tab |
| Source | The script beside a live Markdown preview | — (the input, not a stage) | Source editor notes |
| Markdown AST | The parsed Markdown tree | MarkdownAstProjection |
This note |
| Dialogue AST | The transpiler's tree | DialogueAstProjection |
AST Stage Tabs |
| Desugared AST | The normalized tree | DialogueAstProjection |
AST Stage Tabs |
| Semantic Model | The scene tree and three lookup tables | SemanticProjection |
Semantic Model Visualization Tab |
| Dialogue Graph | The flow graph a runtime walks | GraphProjection |
Dialogue Graph Visualization Tab |
| Playbook | The serialized playbook JSON and its tables | PlaybookProjection |
Playbook Tab |
Config appears only when the report has a configuration context, Source only when it has a script, and Playbook only when it was built through the compiler; a bare single-graph render has none of them. The report opens on Source.
Architecture
flowchart LR
S["script"] --> C["IScriptCompiler.Compile<br/>(once)"]
C --> R["CompilationResult"]
R --> P["stage projections"]
P --> W["GraphWalk"]
W --> DG["DisplayGraph[]"]
R --> X["ConfigurationProjection · PlaybookProjection ·<br/>DiagnosticProjection · SymbolProjection ·<br/>SemanticTokenProjection"]
DG --> PAY["report payload"]
X --> PAY
PAY --> HTML["HtmlRenderer → report"]
DG --> DOT["DotRenderer → ddown compile --emit dot"]
CompilationVisualizer.BuildContent compiles once and projects everything the report needs from
that one result, so the stages, the editor's diagnostics and highlighting, the Config tab, and the
Playbook tab never disagree. The payload is { source, stages, path, mode, project, symbols, configuration, playbook, diagnostics, … }; see Report in model.ts.
The walk is one generic routine:
Walk(root, projection):
visited = {} # node identity -> display id
Visit(node):
if node in visited: return visited[node] # caller adds a Reference edge
add DisplayNode(projection.Describe(node)); visited[node] = it
for neighbor in projection.Neighbors(node):
kind = neighbor already visited ? Reference : Child
add edge(node -> Visit(neighbor), kind)
The Dialogue Graph is the exception: it is a flat node list with cycles and unreachable nodes, so
GraphProjection emits every node in graph order and resolves each edge by id, with a
SpanningTree naming one Child parent per node for the client's tree layout.
Key design decisions
D1 — A separate project keeps the core pure
The core DialogueDown library stays engine-agnostic. The visualizer lives in
DialogueDown.Visualization, which references the core; the core grants it and its test project
InternalsVisibleTo, so the internal IRs are reachable without new public API.
D2 — The visualization layer owns traversal through one projection seam
Each IR family implements one INodeProjection<TNode>, and extension methods
(ir.ToDisplayGraph(source)) give a uniform call site. Adding a stage is one projection; the core
AST records carry no traversal code.
D3 — A graph model with a cycle-safe walk
The display model is a graph, and a tree is its acyclic case. The visited set is keyed by reference identity, not value equality: AST nodes are records, and two distinct nodes with equal content must stay two nodes.
D4 — One display model, pluggable renderers
Renderers are IDisplayRenderer strategies over the same display graph. HtmlRenderer builds the
report; DotRenderer backs ddown compile --emit dot. Mermaid appears only as fenced diagrams a
writer authors in the preview — see
Mermaid Authoring Diagrams.
D5 — One self-contained file, built from a TypeScript client
The client in web/ is TypeScript built by Vite, with D3 for layout and interaction, CodeMirror
for editors, Pico.css for chrome, Tippy.js for tooltips, and marked for previews. Exporting inlines
everything into one HTML file that works offline; serving links the assets so a browser caches
them. Mermaid is loaded only for a script that draws a diagram. Dependencies are pinned by
package-lock.json, licenses are listed in web/NOTICE.md, and CI fails if the committed
web/dist/report.html is stale.
D6 — Categories drive one color scheme across stages
A projection tags each node with a category and the client's palette.ts maps it to a color, so a
concept keeps its color from stage to stage: a Markdown code span and the command it compiles to
are both call. Labels stay stage-local — the Markdown AST legend says "Code span". The legend
counts each category, highlights it on hover, and dims it on click.
D7 — The Source tab grounds every stage in the text
The Source tab shows the script in a CodeMirror editor (read-only in View, editable in Edit) beside a live preview. Every graph node carries its span, so the inspector shows the text a node came from and Node Inspector jumps back to it; Jump to Stage goes the other way.
Unavailable stages
The visualizer always compiles stage-boundary (CompilationMode.StageBoundary), whatever
--mode the CLI is given for compile: a valid script yields every stage, and a broken one halts
with its later stages absent. Fail-fast is never used, because it throws instead of returning a
renderable result.
An absent stage is still a tab, carrying an unavailable reason instead of a graph
(DisplayGraph.ForUnavailableStage):
| Stage | Unavailable when | Reason shown |
|---|---|---|
| Desugared AST, Semantic Model | The compile halted before reaching them | This stage is unavailable due to compilation errors. |
| Dialogue Graph | The compile reported any error, even one later stages recovered from | The dialogue graph is built only for a script that compiles without errors. |
D8 — A marker on the stage, not a union
unavailable? is optional on Stage, and an unavailable stage carries empty nodes and edges,
so code that reads an available stage is unchanged.
D9 — aria-disabled, never disabled
A disabled button suppresses pointer events, so its tooltip — the only place the reason appears —
would never show. The tab is grayed, aria-disabled="true", and carries the reason as its
data-tip; a click or Enter is refused, and neither the default nor the remembered tab lands on
it. The diagnostics themselves appear in the Source editor's
diagnostics overlay and the
Problems panel.
Stage tooltips
Each stage tab explains itself on hover. The text is INodeProjection.Description, authored beside
the stage's Title and carried through GraphWalk into DisplayGraph.Description, so a new stage
brings its own tip. The client sets it as the tab's data-tip, drawn by the report's shared Tippy
tooltips. Config, Source, and Playbook are not projected stages, so their tips are constants in
app.ts (CONFIG_TIP, SOURCE_TIP, PLAYBOOK_TIP).
Error and boundary cases
| Case | Behavior |
|---|---|
| Empty script | A lone root with no edges; every renderer draws an empty but valid diagram. |
| Deep nesting | The walk recurses; renderers assume no depth limit. |
| A cycle | A revisit is a Reference edge, so the walk terminates. |
| Special characters in labels | Each renderer escapes for its format. |
| A halted compile | Later stages render as unavailable. |
| A tab that fails to render in the browser | That tab shows an inline error; the others are unaffected. |
The visualizer never mutates an IR and does not throw for well-formed compiler output; a null argument is an ordinary .NET argument exception.
Testability
| Level | Covers |
|---|---|
| .NET — walk and model | Nodes, edges, and child order for a small projected IR; a synthetic cyclic projection terminates with a Reference edge. |
| .NET — projections | One test file per projection: labels, attributes, categories, spans, descriptions. |
| .NET — renderers | Output and escaping for a tiny graph. |
.NET — CompilationVisualizer |
A real script yields the expected tabs; a halted compile yields unavailable placeholders; the payload serializes unavailable and description. |
| Vitest | Pure and DOM-light client modules, including disabled-tab activation and tab selection. |
| Playwright | The built report in Chromium: tabs, graph interaction, tooltips, disabled tabs, and axe (including color contrast, which jsdom cannot measure). |
The client's gates — tsc --noEmit, ESLint, Stylelint, Prettier, Vitest, and the build-freshness
check — run through npm run check and the CI Frontend job; see
CONTRIBUTING.