Semantic Model Visualization Tab
Note
Status: implemented. The Semantic Model tab shows the semantic analyzer's output as an analytics layout: the scene tree as a graph, and the speaker, anchor, and jump-resolution tables stacked beside it, all cross-linked by entity key.
Table of contents
- Goal & scope
- Ubiquitous language
- Functionality checklist
- Design
- Interfaces & abstractions
- The three tables
- Key design decisions
- Error & boundary cases
- Integration
- Testability
- Deferred enhancements
Goal & scope
The semantic analyzer runs after desugaring and exposes a
SemanticModel on the compilation result; this tab shows
it.
The semantic model is not one graph. It is a scene tree (the nested scenes) plus three lookup tables the analysis resolved — speakers, anchors, and jump resolutions. A single graph tab cannot represent that well. This component adds a Semantic tab with an analytics layout:
- the scene tree as the graph in the main (left) area, reusing the existing interactive tree view — each scene expands to the script blocks it owns; and
- the speaker table, anchor table, and jump-resolution table as collapsible table panels stacked down the right column, which as a whole is resizable and can be hidden to give the graph full width.
The tab is cross-linked: hovering a scene, a speaker, or a jump anywhere — a table, a scene node, or a script block in the tree — highlights that same entity everywhere it appears, so a reader can see which scene a jump resolves to, or every line a speaker speaks.
In scope: the C# projection of the semantic model into a scene-tree graph (with each scene's script blocks) plus the three tables, all sharing cross-link keys; the TS analytics-layout tab (graph + resizable, collapsible stacked tables) with cross-link highlighting; and wiring the tab through the existing report payload. Out of scope: changing the analyzer or its model; the flow between nodes, which is the Dialogue Graph tab's; and editing from this tab.
Ubiquitous language
Reuses the Semantic Analyzer's terms so one concept keeps one name across the analyzer, this tab, and the code.
| Term | Meaning |
|---|---|
| Scene tree | The nested scenes and, under each, the script blocks it owns — the tab's graph. A scene's node links to its anchor-table row. |
| Script block | A piece of a scene's content (a line, speaker, choice, jump, tag, …), described exactly as on the Desugared AST tab. |
| Speaker table | Rows of resolved speakers (name, @id, tags, whether default). |
| Anchor table | Rows mapping a scene's #slug anchor to its scene. |
| Jump-resolution table | Rows of each jump and what it resolved to (a scene, a deferred file target, or unresolved). |
| Entity | A cross-referenced thing with a stable identity: a scene or a speaker. Cross-linking highlights an entity everywhere it appears. |
| Entity key | The stable string identifying an entity across the graph and tables, e.g. scene:the-market, speaker:@guide. A scene node or table row is an entity (its entityKey); a jump/speaker block node or table cell references one (its refKey). |
| Table panel | One collapsible container in the right column holding a table; collapses to a labeled horizontal bar. |
Functionality checklist
- [x] A Semantic tab appears after the Desugared AST tab.
- [x] Its main area shows the scene tree as an interactive graph (zoom, pan, fold, full screen, position memory) — reusing the existing tree view.
- [x] Each scene expands to the script blocks it owns (lines, speakers, choices, jumps, …), described exactly as on the Desugared AST tab, expandable and collapsible per node.
- [x] The scene backbone is emphasized — scenes and the root render as larger, thicker-ringed circles joined by bolder edges, so the tab reads as a scene tree with content hanging off it rather than a flat node tree.
- [x] The right column stacks three table panels: speaker, anchor, jump resolution.
- [x] The whole tables column is resizable (drag the divider) and collapsible (a toggle hides it so the graph fills the width); both choices persist across reloads.
- [x] Each table panel collapses to a horizontal bar (title + row count) and expands again; the choice persists across reloads.
- [x] Cross-linking: hovering a scene (row/cell or graph node), a speaker mention in the tree, or a jump highlights the same entity everywhere — the scene in the graph, the anchor table, and its jumps; a speaker across its tree mentions and its speaker row.
- [x] The jump-resolution table shows each jump's resolution kind — a scene link, a deferred file target, or unresolved — with the scene link cross-referencing the scene entity.
- [x] The speaker table marks the default speaker and lists each speaker's tags.
- [x] Empty tables render a clear "none" state, not a blank panel.
- [x] Clicking a scene or block shows its attributes, source, and a rendered preview in a node-details panel pinned to the top of the tables column (sticky); it auto-expands on selection.
- [x] The tab's content is read-only in both View and Edit.
Design
Payload shape
Each stage in the report payload is a graph (title, description, nodes, edges). The
semantic stage is still a graph — the scene tree — so it reuses that, and adds its tables
alongside:
Stagehas an optionaltables?: SemanticTable[]. A stage with notablesrenders as a plain graph tab. A stage withtablesrenders in the analytics layout.
This keeps one uniform tab model: the scene tree flows through the entire existing tree view (camera memory, fold, full screen, cross-stage colors) for free, and the tables are purely additive, with no second tab model.
Cross-linking by entity key
Cross-linking needs a shared identity for each entity that appears in more than one place. The projection emits entity keys:
- A scene-tree graph node for a scene carries
entityKey = scene:<anchor>. - An anchor-table row carries
entityKey = scene:<anchor>(the row is that scene). - A jump-resolution row that resolved to a scene carries, on its target cell,
refKey = scene:<anchor>(it references that scene). - A speaker-table row carries
entityKey = speaker:<id-or-name>.
The TS side builds one index from entityKey/refKey → the DOM elements carrying it.
Hovering any element with a key adds a highlight class to every element sharing that
key, across the graph and all tables; leaving clears it. No positional or title-matching
guesswork — the key is the single source of truth, mirroring how the model itself keys
scenes by anchor and speakers by name/id.
Layout
┌──────────────────────────────┬───────────────────────────┐
│ │ ▸ Speakers (3) │ ← collapsed bar
│ Scene tree ├───────────────────────────┤
│ (graph: zoom, │ ▾ Anchors (4) │
│ pan, fold, │ #the-market → The Market│ ← expanded table
│ full screen) │ #the-forest → The Forest│
│ ├───────────────────────────┤
│ │ ▾ Jumps (2) │
│ │ [east] => The Market │
└──────────────────────────────┴───────────────────────────┘
The right column reuses the collapsible-panel pattern already in the report (the node-inspector collapse from the side-panels work): each table panel has a header bar with a collapse toggle; collapsed, only the bar shows. The three panels stack and scroll independently. For this tab, the tables replace the single node-detail inspector.
Flow
flowchart LR
model["SemanticModel<br/>(compilation result)"] --> proj["SemanticProjection (C#)"]
proj --> tree["Scene-tree graph<br/>(nodes + edges, entity keys)"]
proj --> tables["3 tables<br/>(rows + entity/ref keys)"]
tree --> stage["Stage { nodes, edges, tables }"]
tables --> stage
stage --> json["report JSON"]
json --> view["createSemanticView (TS)"]
view --> graph["tree view (scene tree)"]
view --> panels["stacked collapsible table panels"]
graph --> link["entity-key highlight index"]
panels --> link
Interfaces & abstractions
| Type / function | Responsibility | Collaborators |
|---|---|---|
SemanticProjection (C#) |
Project a SemanticModel (and the source text) into a scene-tree DisplayGraph plus the three SemanticTables, all sharing cross-link keys. |
SceneTreeProjection, GraphWalk |
SceneTreeProjection : INodeProjection<object> (C#) |
Describe/'Neighbors' a scene (its blocks then its subscenes) and delegate each script block to the shared DialogueAstProjection, adding a RefKey on a speaker mention or a scene-resolving jump. |
GraphWalk, DialogueAstProjection, SpeakerTable, JumpResolutionTable |
NodeDescription.TypeName / DisplayNode.TypeName (C#) |
Optional legend name for a node whose label is content (a scene title) rather than a type; the legend groups by it when present. | GraphWalk, createLegend |
NodeDescription.RefKey / DisplayNode.RefKey (C#) |
Optional cross-link key when a node references an entity — a jump's target scene, a speaker mention — the symmetric partner of EntityKey. |
GraphWalk, createEntityHighlighter |
SemanticTable / SemanticRow / SemanticCell (C#) |
A serializable table: title, columns, and rows of cells; a cell may carry entityKey/refKey. |
DisplayGraphJson |
Stage.tables (TS + C# payload) |
Optional tables riding alongside a stage's graph; absent ⇒ a plain graph tab. | addStageTab, DisplayGraphJson |
createSemanticView (TS) |
Build the analytics tab: the scene-tree tree view + stacked table panels, wired for cross-link highlight, with a draggable divider that resizes and hides the tables column. | createTreeView, createTablePanel, createEntityHighlighter, initCollapsiblePanel |
createTablePanel (TS) |
Render one SemanticTable as a collapsible panel (header bar + table) carrying the cross-link keys. |
initCollapsiblePanel |
createNodeDetailPanel (TS) |
The sticky node-details panel: a collapsible panel pinned to the top of the tables column that shows a clicked node's attributes, source, and preview (auto-expanding on select). | initCollapsiblePanel, nodeDetailTitle/nodeDetailBody |
nodeDetailTitle / nodeDetailBody (TS) |
Shared node-detail rendering (category dot + label; attributes + source + preview), reused by both the standard #detail inspector and the sticky panel. |
renderMarkdown |
createEntityHighlighter (TS) |
Index elements by entityKey/refKey and toggle the shared entity-highlight class on hover. |
— |
The three tables
| Table | One row per | Columns | Entity / ref keys |
|---|---|---|---|
| Speakers | resolved speaker | Name, @id, Tags, Default |
row entityKey = speaker:<id-or-name> |
| Anchors | scene with an anchor | Anchor (#slug), Scene (heading text), Level |
row entityKey = scene:<anchor> |
| Jump resolutions | analyzed jump | Jump (label), Target (#slug), Resolves to (scene / file — deferred / unresolved) |
"Resolves to" cell refKey = scene:<anchor> for a scene jump |
Key design decisions
- The scene tree is the stage's graph; tables ride in
Stage.tables. Reuses the whole tree view and keeps one tab model; a table-less stage is unchanged. (See Payload shape.) - Each scene expands to its script blocks, reusing the Desugared AST projection.
SceneTreeProjectionis a composite overobject: a scene yields its blocks then its subscenes, and every block is described by the sharedDialogueAstProjection, so the tree reads identically to the Desugared AST tab without duplicating that logic. - Cross-link by entity key, not by matching. The projection emits
scene:<anchor>andspeaker:<id-or-name>keys; the UI highlights by exact key. A scene node or table row is an entity (entityKey); a jump/speaker block node or table cell references one (refKey) — the highlighter treats the two symmetrically. This mirrors the model's own keying and avoids brittle title/position matching. (See Cross-linking.) - Reuse the collapsible-panel pattern for both the table stack and the whole column. Each table panel and the column as a whole reuse the report's collapse toggle (with persistence); a pointer-captured divider resizes the column, mirroring the Source tab's editor/preview split rather than inventing a new affordance.
- A dedicated
SemanticProjection, exposed through the existing friend seam. The model isinternal; the visualization project already has friend access viaCompilationResult.Semantics, so the projection lives on the visualization side with no new public surface. The scene-tree graph goes throughGraphWalklike every other stage. - Hover to highlight. Cross-linking follows the pointer; a sticky click-to-pin selection is an open question (see Deferred enhancements).
- A node's legend name can differ from its label. A scene node labels itself by its
title (so the tree reads as scenes), which would make the legend list every title.
So a node carries an optional
TypeName("Scene", "Document") that the legend groups and labels by, falling back to the label for AST stages that label themselves by type. - Emphasize the scene backbone. The tree view tags scene/root nodes (those with a
typeName) and the edges into scenes with asceneclass and draws scene circles larger; CSS then bolds their ring and edges. Only the semantic tab setstypeName, so the shared tree view leaves every other tab unchanged.
Error & boundary cases
| Case | Behavior |
|---|---|
| No scenes (flat document, root only) | The scene tree shows just the root; the anchor table shows its "No scenes." note. |
| No speakers beyond the default | The speaker table shows the default speaker row only. |
| No jumps | The jump-resolution table shows its "No jumps." note. |
| Jump resolved to a file target (deferred) | Row shows the file/anchor as text with a "deferred" note; no scene refKey. |
| Unresolved jump (empty target) | Row shows "unresolved"; no refKey. |
| A scene whose heading text is empty after slugging | Cannot happen — the analyzer rejects empty-slug headings upstream; the projection assumes a valid model. |
| Duplicate-looking headings | The analyzer already made anchors unique (or threw); the projection trusts the model's keys. |
Integration
- C#:
CompilationVisualizeradds the semantic stage after the three AST stages, projectingresult.SemanticsthroughSemanticProjection.DisplayGraphJsonserializes the newtablesfield (omitted when null, like other optional payload fields). - TS:
runApp'saddStageTabroutes a stage that hastablestocreateSemanticViewinstead of the plain graph path; the scene tree still usescreateTreeView, so camera memory, fold, and full screen work unchanged. The tab hides the shared node-detail inspector; clicking a node instead fills its own node-details panel, pinned (sticky) to the top of the tables column and reusing the shared detail rendering. - The report stays a single self-contained offline file; no new runtime dependency.
Note
Rendering: the scene-tree SVG must sit in an out-of-flow, contained paint
context — .semantic-graph svg.tree is position: absolute; inset: 0 and the column
uses contain: layout paint, mirroring how every other stage graph lives in an absolute
section.stage. As an in-flow flex item beside the scrollable tables, Safari's GPU
compositor left ghost copies of the tree while zooming (the previous frame's layer was
not fully invalidated). Headless browsers use software rendering and cannot reproduce it,
so verify zoom in real Safari when touching this layout.
Testability
- C# projection — unit-test
SemanticProjectionagainst a compiled sample: assert the scene-tree graph's nodes/edges and entity keys, and each table's rows/cells and entity/ref keys.SceneTreeProjectionis tested throughGraphWalklike the AST projections. - TS — unit-test
createEntityHighlighter(hovering a key highlights all elements sharing it) and the table/panel rendering (jsdom). End-to-end (Playwright): the Semantic tab shows the graph + three collapsible tables; hovering an anchor row highlights the matching scene node and jump row; a table collapses to its bar and reopens. - Mirror the one-file-per-source layout; high, meaningful coverage as elsewhere.
Deferred enhancements
Two enhancements are deliberately not built:
- Click-to-pin selection. Cross-linking is hover-to-highlight only. A sticky click-to-pin selection (so a reader can move the mouse away while comparing) is an open question.
- Scene-to-speaker aggregation. A speaker cross-links from each of its mentions in the tree and its speaker row. Tagging each scene node with the speakers that appear in it (so hovering a speaker lights up whole scenes, not just the mentions) is deferred — it needs the projection to aggregate speakers per scene.