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:
@@ -29,6 +29,7 @@ import {
|
||||
SearchPalette,
|
||||
SymbolView,
|
||||
TrailBar,
|
||||
TypeHierarchy,
|
||||
createHttpAdapter,
|
||||
fileHref,
|
||||
flowHref,
|
||||
@@ -47,6 +48,7 @@ import {
|
||||
type WireNodeRef,
|
||||
type WireSource,
|
||||
type WireStats,
|
||||
type WireHierarchy,
|
||||
type WireSymbolPayload,
|
||||
} from '../ui/src/index';
|
||||
|
||||
@@ -132,6 +134,7 @@ const SYMBOL: WireSymbolPayload = {
|
||||
],
|
||||
},
|
||||
typesUsed: [],
|
||||
hierarchy: null,
|
||||
counts: { callers: 1, callees: 1, typesUsed: 0, fanIn: 1, fanOut: 1, members: 0, hub: false },
|
||||
tests: { reached: false, hops: null, fileCount: 0, files: [], exhaustive: true, hopsSearched: 3 },
|
||||
outsideIndex: { total: 0, byKind: {}, samples: [] },
|
||||
@@ -489,6 +492,55 @@ describe('@colbymchenry/codegraph-ui — a host renders the package', () => {
|
||||
expect(text.toLowerCase()).toContain('test');
|
||||
});
|
||||
|
||||
it('TypeHierarchy draws the fan, its wiring and its fold from a payload alone', async () => {
|
||||
const implementers = Array.from({ length: 14 }, (_, i) => ({
|
||||
id: `impl-${i}`,
|
||||
kind: 'class' as const,
|
||||
name: `Target${i}`,
|
||||
qualifiedName: `Target${i}`,
|
||||
file: `src/targets/target-${i}.ts`,
|
||||
line: 1,
|
||||
endLine: 9,
|
||||
language: 'typescript' as const,
|
||||
test: false,
|
||||
depth: 1,
|
||||
parentId: SYMBOL.node.id,
|
||||
relation: 'implements' as const,
|
||||
// The first one arrived through a resolver rather than a parse, which is
|
||||
// the case the block has to draw differently.
|
||||
synthesized: i === 0,
|
||||
...(i === 0 ? { via: 'go-implements', registeredAt: 'src/clock.go:11' } : {}),
|
||||
hiddenSubtypes: 0,
|
||||
}));
|
||||
const hierarchy: WireHierarchy = {
|
||||
ancestors: { total: 0, shown: 0, truncated: false, items: [] },
|
||||
descendants: {
|
||||
total: implementers.length,
|
||||
shown: implementers.length,
|
||||
truncated: false,
|
||||
items: implementers,
|
||||
},
|
||||
direct: implementers.length,
|
||||
implementers: implementers.length,
|
||||
bounded: false,
|
||||
polymorphic: true,
|
||||
};
|
||||
|
||||
await render(TypeHierarchy, { hierarchy, focus: SYMBOL.node, onopen: () => {} });
|
||||
|
||||
const text = host.textContent ?? '';
|
||||
// The claim a reader cannot get by counting rows.
|
||||
expect(text).toContain('14 implementations');
|
||||
// The wiring site of the synthesized edge.
|
||||
expect(text).toContain('go-implements');
|
||||
// Twelve rows, then the fold — never a silent truncation.
|
||||
expect(text).toContain('+2 more implementations');
|
||||
expect(text).toContain('Target0');
|
||||
expect(text).not.toContain('Target13');
|
||||
// It draws no network of its own: this component was handed a payload.
|
||||
expect(host.querySelectorAll('path').length).toBe(12);
|
||||
});
|
||||
|
||||
it('FlowStrip draws one card per hop from a mock adapter', async () => {
|
||||
const { adapter, calls } = mockAdapter();
|
||||
setGraphAdapter(adapter);
|
||||
|
||||
Reference in New Issue
Block a user