mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-11-08 09:23:05 +08:00
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:
@@ -23,6 +23,7 @@
|
||||
import type { CodeGraph } from '../../index';
|
||||
import type { Edge, Node, NodeKind } from '../../types';
|
||||
import { isTestFile } from '../../search/query-utils';
|
||||
import { buildHierarchy, type WireOverride } from './hierarchy';
|
||||
import { notFound } from './respond';
|
||||
import { findIndexedFile, hasDriftedOnDisk } from './source';
|
||||
import {
|
||||
@@ -64,6 +65,12 @@ export interface WireMember extends WireNodeRef {
|
||||
*/
|
||||
fanIn: number;
|
||||
fanOut: number;
|
||||
/**
|
||||
* This member redeclares one an ancestor type declares — a name match inside
|
||||
* a chain the graph already links, not an `overrides` edge (nothing emits
|
||||
* one). Absent for every member that declares something new.
|
||||
*/
|
||||
overrides?: WireOverride;
|
||||
}
|
||||
|
||||
export function buildNode(cg: CodeGraph, projectRoot: string, nodeId: string): unknown {
|
||||
@@ -155,7 +162,10 @@ export function buildNode(cg: CodeGraph, projectRoot: string, nodeId: string): u
|
||||
// ---------------------------------------------------------------------------
|
||||
// Members outline
|
||||
// ---------------------------------------------------------------------------
|
||||
const members = buildMembers(cg, node, containsOut, endpoints);
|
||||
// The type hierarchy, and the override marks it puts on the outline. Gated
|
||||
// to types inside `buildHierarchy`, so a function costs one kind test.
|
||||
const hierarchy = buildHierarchy(cg, node);
|
||||
const members = buildMembers(cg, node, containsOut, endpoints, hierarchy?.overrides);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Counts, tests, what leaves the index, blast radius
|
||||
@@ -176,6 +186,11 @@ export function buildNode(cg: CodeGraph, projectRoot: string, nodeId: string): u
|
||||
/** Outermost first: file, then module/class, then the symbol's own parent. */
|
||||
ancestors: [...ancestors].reverse().map(toNodeRef),
|
||||
members: wireList(members.items, members.total),
|
||||
/**
|
||||
* Ancestors, subtypes and the dispatch fan — `null` for anything that is
|
||||
* not a type, and for a type with no hierarchy at all.
|
||||
*/
|
||||
hierarchy: hierarchy?.wire ?? null,
|
||||
incoming: wireList(shownIncoming, incomingGroups.length),
|
||||
outgoing: wireList(shownOutgoing, outgoingGroups.length),
|
||||
/** `references` edges into a type — the header's "uses types …" chips. */
|
||||
@@ -218,7 +233,8 @@ function buildMembers(
|
||||
cg: CodeGraph,
|
||||
focal: Node,
|
||||
containsOut: readonly Edge[],
|
||||
endpoints: Map<string, Node>
|
||||
endpoints: Map<string, Node>,
|
||||
overrides?: Map<string, WireOverride>
|
||||
): { items: WireMember[]; total: number } {
|
||||
const direct: Array<{ node: Node; parentId: string; depth: number }> = [];
|
||||
for (const edge of containsOut) {
|
||||
@@ -252,13 +268,18 @@ function buildMembers(
|
||||
const fanOut = cg.getFanOut(memberIds);
|
||||
|
||||
return {
|
||||
items: shown.map((entry) => ({
|
||||
...toNodeRef(entry.node),
|
||||
parentId: entry.parentId,
|
||||
depth: entry.depth,
|
||||
fanIn: fanIn.get(entry.node.id) ?? 0,
|
||||
fanOut: fanOut.get(entry.node.id) ?? 0,
|
||||
})),
|
||||
items: shown.map((entry) => {
|
||||
const member: WireMember = {
|
||||
...toNodeRef(entry.node),
|
||||
parentId: entry.parentId,
|
||||
depth: entry.depth,
|
||||
fanIn: fanIn.get(entry.node.id) ?? 0,
|
||||
fanOut: fanOut.get(entry.node.id) ?? 0,
|
||||
};
|
||||
const override = overrides?.get(entry.node.id);
|
||||
if (override) member.overrides = override;
|
||||
return member;
|
||||
}),
|
||||
total: all.length,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user