feat(ui): the type hierarchy — what a type is built on, and what dispatches through it (CG-58)

A vertical tree above the members outline for classes, interfaces, structs,
traits, protocols, enums, unions and type aliases: ancestors above (the whole
chain, not just the direct parent), the focus in accent, subtypes below indented
per level. `extends` draws solid, `implements` dashed; a synthesized edge — Go's
implicit interface satisfaction — draws dashed wider and carries the site it was
wired at, so a relation the resolver inferred never reads like one the source
wrote down. For an interface the fan below IS the set of runtime targets a call
can land on, and a type with eight or more implementers leads with that in a
sentence. Members that redeclare an ancestor's are marked in the outline.

The walk lives in `src/graph/type-hierarchy.ts`, following CG-50/CG-51: shared
computation in `src/graph/`, presentation in the caller. Its `countImplementers`
is now also what `ToolHandler.buildPolymorphicBoundaries` counts with, so "N
types implement X" is the same N whether an agent reads it or a person does.
`/api/node` carries the block as `hierarchy` rather than a second endpoint —
it is part of the Symbol view's first paint, and gated to types, so a function
costs one kind test.

Layout is arithmetic (24px rows, 22px indent, orthogonal connectors computed
from the two): no ResizeObserver, same payload → same picture. The header's
`extends X` / `implemented by …` chips are suppressed while the tree is on
screen — two renderings of one relation in one column is how a reader ends up
trusting neither.

`TypeHierarchy` is exported from `@colbymchenry/codegraph-ui` and takes its data
as a prop, so a host holding a `WireSymbolPayload` renders it without a second
read.
This commit is contained in:
Colby McHenry
2026-08-27 07:14:55 -05:00
parent c15413f200
commit 2a0c6dc58f
21 changed files with 2100 additions and 28 deletions
+39 -5
View File
@@ -63,11 +63,17 @@ build so that mistake cannot land twice.
```
Exports: `SymbolView`, `FlowStrip`, `ArchitectureMap`, `FileView`,
`FileSourceView`, `EntryPointsView`, `TrailBar`, `SearchPalette`,
`PalettePanel`, `PaletteRows`, `DriftBanner`, `KindGlyph`, `ExportButtons`,
`CodegraphUi` — plus every pure model function the screens are built from
(`buildCalleeRail`, `buildFlowLayout`, `buildMapLayout`, `tokensByLine`, …) and
the `Wire*` types an adapter answers in.
`FileSourceView`, `EntryPointsView`, `TypeHierarchy`, `TrailBar`,
`SearchPalette`, `PalettePanel`, `PaletteRows`, `DriftBanner`, `KindGlyph`,
`ExportButtons`, `CodegraphUi` — plus every pure model function the screens are
built from (`buildCalleeRail`, `buildFlowLayout`, `buildMapLayout`,
`buildHierarchyModel`, `tokensByLine`, …) and the `Wire*` types an adapter
answers in.
`TypeHierarchy` is the one screen that takes its data as a prop rather than
asking the adapter: it is part of `SymbolView`'s payload (`/api/node`'s
`hierarchy`), so a host that already holds a `WireSymbolPayload` can render the
tree on its own without a second read.
### The adapter is the only way data arrives
@@ -254,6 +260,34 @@ The verdict itself is not computed here or in the server: it is
`findDynamicBoundaries` in `src/graph/dynamic-boundary-report.ts`, the same
detector `codegraph_explore` announces boundaries with.
## The type hierarchy
A class, interface, struct, trait or enum carries a `hierarchy` on its
`/api/node` payload: ancestors up, subtypes down, and the fan an interface call
dispatches into. `buildHierarchyModel` turns it into a tree whose geometry is
arithmetic — 24px rows, 22px of indent per descendant level, orthogonal 1px
connectors computed from those two numbers. Nothing is measured; the same
payload always draws the same picture.
The details worth knowing before changing it:
- **`extends` is solid, `implements` dashed `4 3`, a synthesized edge dashed
`6 3`** with a `via <mechanism>` pill. In Go, `System` satisfies `Clock`
without either file naming the other and the edge exists only because the
resolver made it — the block says so rather than drawing it like a parse.
- **Overrides on the members outline are a NAME match**, not an `overrides`
edge (nothing in the engine emits one). They are matched against the nearest
ancestor that declares the name and are blind to signatures, and the tooltip
says which claim was actually checked.
- **The fold trims the deepest end**, because the walk is breadth-first: a
reader looking at an interface gets every direct implementation before any
subclass of one appears at all.
The walk is not computed here or in the server: it is `buildTypeHierarchy` in
`src/graph/type-hierarchy.ts`, whose `countImplementers` is also the number
`codegraph_explore` prints when it announces an interface dispatch — so "N types
implement X" is the same N wherever you read it.
## Live updates
The viewer never polls. `lib/live.svelte.ts` holds one `EventSource` on