fix(ui): keep the architecture overview readable inside large areas

Opening "django" in the Overview jumped straight to its files: areas were
one folder deep except under src, lib and similar containers. The folder
layout was computed for every file while only 40 were drawn, so the
pruned platforms kept their full size, filled the canvas and flattened
the tilted view; full-path labels overlapped and heights were scaled
against the largest repository area.

An opened area now shows its next folder level as area blocks, applying
the repository area rule to the path below it (so graph-ui shows src/app
as one step, and django shows contrib, db, utils ...). The node cap runs
before the folder layout, so platforms are sized to what is drawn; a
filter that only narrows the drawn map keeps its positions. Outside areas
are named as siblings of the opened one and sit in their own lane in front
of its platform. Labels are relative to the opened area with the full
path in the tooltip, heights compare the bricks of the current scope, and
the Overview uses adaptive labels whose culling measures the real label
boxes instead of the larger service-map estimate. The location trail
offers every drill-in level.

Signed-off-by: Bernhard Jackiewicz <bj@techport.io>
This commit is contained in:
Bernhard Jackiewicz
2026-10-02 19:13:32 +02:00
parent 8961287899
commit 2e0b8eaa35
11 changed files with 301 additions and 65 deletions
+49 -17
View File
@@ -137,6 +137,8 @@ function ArchitectureNode({ node, selected, highlighted, dimmed, labelVisible =
const roofLanguages = node.tint && mixed ? measure!.languages.slice(0, 4) : [];
const roofTotal = measure?.lines || measure?.files || 1;
let roofOffset = -size[0] / 2;
// Labels name a part relative to the opened area; the tooltip keeps its full path.
const path = node.kind === 'area' || node.kind === 'file' ? node.filePath ?? node.areaPath : undefined;
return <group position={node.position}>
<mesh position={[0, size[1] / 2, 0]}
onClick={event => { event.stopPropagation(); onSelect(); }}
@@ -168,7 +170,7 @@ function ArchitectureNode({ node, selected, highlighted, dimmed, labelVisible =
data-node-id={node.id} data-position={node.position.join(',')} data-label-visible={labelVisible}
data-lines={measure?.lines} data-height={size[1]} data-language={measure?.languages[0]?.name} data-gravity={node.gravity ?? 0} data-size-percent={node.sizePercent} data-gravity-percent={node.gravityPercent}
onPointerEnter={() => onHover(node.id)} onPointerLeave={() => onHover()}
title={`${node.label}\n${node.detail}${measure ? `\n${lineText}\n${measure.languages.map(language => language.name).join(', ')} (from file types)` : ''}`}>
title={`${node.label}${path && path !== node.label ? `\n${path}` : ''}\n${node.detail}${measure ? `\n${lineText}\n${measure.languages.map(language => language.name).join(', ')} (from file types)` : ''}`}>
{selected && <span className="architecture-node-kind"><i style={{ background: color }} />{node.kindLabel ?? (node.kind === 'area' ? 'Source area' : node.kind)}</span>}
<span className="architecture-node-title"><strong>{node.hotspot && <span className="architecture-hotspot-mark" title={`${node.hotspot.findings.length} ranked hotspots; strongest measured fan-in ${node.hotspot.maxFanIn ?? 'unavailable'}${node.gravityPercent === undefined ? '' : `; gravity ${Number(node.gravityPercent.toFixed(1))}%`}`}>◉ </span>}{node.label}</strong>{!measure && node.gravityPercent !== undefined && <small className="architecture-node-size" title="Gravity relative to strongest hotspot">{node.gravityPercent > 0 && node.gravityPercent < 0.1 ? '<0.1' : Number(node.gravityPercent.toFixed(1))}%</small>}</span>
{selected && measure && <span className="architecture-node-metric"><i style={{ background: color }} />{measure.lines === undefined ? '?' : `${measure.measuredFiles < measure.files ? '≥' : ''}${Intl.NumberFormat('en', { notation: 'compact', maximumFractionDigits: 1 }).format(measure.lines)}`} lines{mixed ? ' · mixed' : ''}</span>}
@@ -178,26 +180,48 @@ function ArchitectureNode({ node, selected, highlighted, dimmed, labelVisible =
</group>;
}
/** Screen-space label density changes with zoom; source positions never do. */
/**
* Screen-space label density changes with zoom; source positions never do.
* Label boxes are measured once per model, so culling follows the CSS of the
* map that shows them (the compact Overview labels are far smaller than the
* service map's); the estimate only covers a label not laid out yet.
*/
function HotspotLabels({ model, priorityId, onVisible }: { model: RenderGraph; priorityId?: string; onVisible: (ids: Set<string>) => void }) {
const { camera, size } = useThree();
const { camera, size, gl, events, invalidate } = useThree();
// Html labels mount where drei puts them: the event source, else the canvas parent.
const labelHost = (events.connected as HTMLElement | undefined) ?? gl.domElement.parentElement;
const previous = useRef('');
const boxes = useRef(new Map<string, [number, number]>());
const attempts = useRef(0);
useEffect(() => { boxes.current.clear(); attempts.current = 0; }, [model]);
useFrame(() => {
const unmeasured = () => model.nodes.some(node => !boxes.current.has(node.id));
if (unmeasured()) labelHost?.querySelectorAll<HTMLElement>('.architecture-node-label[data-node-id]').forEach(label => {
// A selected label carries extra rows; it is the priority label and keeps its reserve below.
if (label.offsetWidth && !label.classList.contains('is-selected')) boxes.current.set(label.dataset.nodeId!, [label.offsetWidth, label.offsetHeight]);
});
const occupied: { left: number; right: number; top: number; bottom: number }[] = [];
const ids = new Set<string>();
const ordered = [...model.nodes].sort((a, b) => Number(b.id === priorityId) - Number(a.id === priorityId));
// Hotspot wells, then larger parts, claim their label space first.
const ordered = [...model.nodes].sort((a, b) => Number(b.id === priorityId) - Number(a.id === priorityId)
|| (b.gravity ?? 0) - (a.gravity ?? 0) || b.count - a.count);
for (const node of ordered) {
const point = new Vector3(...node.position).add(new Vector3(0, dimensions(node)[1] + 7, 0)).project(camera);
const x = (point.x + 1) * size.width / 2; const y = (1 - point.y) * size.height / 2;
const width = Math.min(178, Math.max(78, node.label.length * 7 + (node.gravityPercent === undefined ? 26 : 68)));
const halfHeight = node.id === priorityId ? 48 : 19;
const rect = { left: x - width / 2 - 5, right: x + width / 2 + 5, top: y - halfHeight, bottom: y + halfHeight };
const box = boxes.current.get(node.id);
const width = box?.[0] ?? Math.min(178, Math.max(78, node.label.length * 7 + (node.gravityPercent === undefined ? 26 : 68)));
const halfHeight = node.id === priorityId ? 48 : (box?.[1] ?? 38) / 2;
// A measured label may overlap a neighbour by its padding, never by its text.
const [insetX, insetY] = box ? [6, 4] : [-5, 0];
const rect = { left: x - width / 2 + insetX, right: x + width / 2 - insetX, top: y - halfHeight + insetY, bottom: y + halfHeight - insetY };
if (node.id !== priorityId && (rect.right < 0 || rect.left > size.width || rect.bottom < 0 || rect.top > size.height
|| occupied.some(other => rect.left < other.right && rect.right > other.left && rect.top < other.bottom && rect.bottom > other.top))) continue;
ids.add(node.id); occupied.push(rect);
}
const key = [...ids].sort().join('|');
if (previous.current !== key) { previous.current = key; onVisible(ids); }
// Labels mount a moment after the scene; a few extra frames pick up their real size.
if (unmeasured() && attempts.current++ < 8) invalidate();
});
return null;
}
@@ -256,16 +280,24 @@ function FitArchitecture({ model, planar, resetKey, controls, active }: {
}
export function ArchitectureScene({ model: graphModel, selectedId, selectedEdgeId, onSelect, onSelectEdge, onOpen, onClearSelection, active = true, planar = false, resetKey = 0, catalog, heightMetric = 'uniform', colorMetric = 'kind', hotspots, showHotspots = true, adaptiveLabels = false }: ArchitectureSceneProps) {
const model: RenderGraph = useMemo(() => ({ ...graphModel, nodes: graphModel.nodes.map(node => {
const measure = catalog ? measureSourceNode(node, catalog) : undefined;
const sizePercent = catalog ? sourceNodeSizePercent(node, catalog) : undefined;
const language = measure?.languages[0]?.name ?? (node.filePath ? sourceLanguage(node.filePath) : undefined);
const hotspot = showHotspots && hotspots ? hotspotsForNode(node, hotspots) : undefined;
return { ...node, measure, sizePercent, hotspot, gravity: hotspot && hotspots ? gravityStrength(hotspot.maxFanIn, hotspots.maxFanIn) : 0,
gravityPercent: hotspot && hotspots ? gravityPercent(hotspot.maxFanIn, hotspots.maxFanIn) : undefined,
height: catalog && heightMetric === 'lines' && ['area', 'file'].includes(node.kind) ? node.kind === 'area' && node.areaPath === '(root)' ? 0.35 : sourceBrickHeight(sizePercent, 100) : undefined,
tint: node.tint ?? (colorMetric === 'language' && node.kind !== 'route' ? languageColor(language ?? 'Unknown') : undefined) };
}) }), [graphModel, catalog, heightMetric, colorMetric, hotspots, showHotspots]);
const model: RenderGraph = useMemo(() => {
const measures = new Map(graphModel.nodes.map(node => [node.id, catalog ? measureSourceNode(node, catalog) : undefined]));
// Heights compare the bricks of this scope with each other. Root files are
// the flat baseline; outside areas are context and stay flat as well.
const rootFiles = (node: SemanticNode) => node.kind === 'area' && node.areaPath === '(root)';
const reference = Math.max(1, ...graphModel.nodes.filter(node => ['area', 'file'].includes(node.kind) && !node.external && !rootFiles(node))
.map(node => measures.get(node.id)?.lines ?? 0));
return { ...graphModel, nodes: graphModel.nodes.map(node => {
const measure = measures.get(node.id);
const sizePercent = catalog ? sourceNodeSizePercent(node, catalog) : undefined;
const language = measure?.languages[0]?.name ?? (node.filePath ? sourceLanguage(node.filePath) : undefined);
const hotspot = showHotspots && hotspots ? hotspotsForNode(node, hotspots) : undefined;
return { ...node, measure, sizePercent, hotspot, gravity: hotspot && hotspots ? gravityStrength(hotspot.maxFanIn, hotspots.maxFanIn) : 0,
gravityPercent: hotspot && hotspots ? gravityPercent(hotspot.maxFanIn, hotspots.maxFanIn) : undefined,
height: catalog && heightMetric === 'lines' && ['area', 'file'].includes(node.kind) && !node.external ? rootFiles(node) ? 0.35 : sourceBrickHeight(measure?.lines, reference) : undefined,
tint: node.tint ?? (colorMetric === 'language' && node.kind !== 'route' ? languageColor(language ?? 'Unknown') : undefined) };
}) };
}, [graphModel, catalog, heightMetric, colorMetric, hotspots, showHotspots]);
const [hoveredId, setHoveredId] = useState<string>();
const [hoveredEdgeId, setHoveredEdgeId] = useState<string>();
const [visibleHotspotLabels, setVisibleHotspotLabels] = useState<Set<string>>();
@@ -32,7 +32,7 @@ it('drills inferred areas to files and symbols before enabling real-node impact
await click('src/api');
expect(onSelect).not.toHaveBeenCalled();
expect(host.textContent).not.toContain('Analyze selected symbol');
await click('Open area →'); await click('src/api/main.ts'); await click('Open file symbols →'); await click('start');
await click('Open area →'); await click('main.ts'); await click('Open file symbols →'); await click('start');
expect(onSelect).toHaveBeenCalledExactlyOnceWith(node);
expect(host.textContent).toContain('Analyze selected symbol');
await click('Open source'); expect(onNavigate).toHaveBeenCalledWith('src/api/main.ts', 12, 'start');
@@ -40,13 +40,27 @@ it('drills inferred areas to files and symbols before enabling real-node impact
it('returns from a file symbol to the repository map on empty background', async () => {
const clear = vi.fn();
await act(async () => root.render(<SpatialArchitecture project="sample" graph={graph} overview={overview} view="overview" filter="" active onSelect={vi.fn()} onClearSelection={clear} onNavigate={vi.fn()} onView={vi.fn()} selectionPanel={<button>Analyze selected symbol</button>} />));
await click('src/api'); await click('Open area →'); await click('src/api/main.ts'); await click('Open file symbols →'); await click('start');
await click('src/api'); await click('Open area →'); await click('main.ts'); await click('Open file symbols →'); await click('start');
expect(host.textContent).toContain('Analyze selected symbol');
await act(async () => host.querySelector<HTMLButtonElement>('[aria-label="Empty background"]')!.click());
expect(host.querySelector('[data-testid="scene"]')?.textContent).toContain('src/api');
expect(host.textContent).not.toContain('Analyze selected symbol');
expect(clear).toHaveBeenCalledOnce();
});
it('opens nested areas level by level and walks back through the location trail', async () => {
const nested: GraphData = { nodes: [{ ...node, file_path: 'django/contrib/admin/options.py' }, { ...node, id: 2, name: 'render', qualified_name: 'sample.render', file_path: 'django/shortcuts.py' }], edges: [], total_nodes: 2 };
await act(async () => root.render(<SpatialArchitecture project="sample" graph={nested} overview={overview} view="overview" filter="" active onNavigate={vi.fn()} onView={vi.fn()} />));
const scene = () => [...host.querySelectorAll('[data-testid="scene"] [data-node]')].map(item => item.textContent);
const trail = () => [...host.querySelectorAll('[aria-label="Architecture location"] button')].map(item => item.textContent);
await click('django'); await click('Open area →');
expect(scene()).toEqual(['contrib', 'shortcuts.py']);
await click('contrib'); await click('Open area →');
expect(scene()).toEqual(['admin']);
expect(trail()).toEqual(['sample', 'django', 'contrib']);
const django = [...host.querySelectorAll<HTMLButtonElement>('[aria-label="Architecture location"] button')].find(item => item.textContent === 'django')!;
await act(async () => django.click());
expect(scene()).toEqual(['contrib', 'shortcuts.py']);
});
it('publishes source-area evidence without inventing a symbol and clears it on background', async () => {
const onSelectionEvidence = vi.fn();
await act(async () => root.render(<SpatialArchitecture project="sample" generation="g1" graph={graph} overview={overview} view="overview" filter="" active onSelectionEvidence={onSelectionEvidence} onNavigate={vi.fn()} onView={vi.fn()} />));
@@ -1,10 +1,11 @@
import { Component, useEffect, useMemo, useState, type ReactNode } from 'react';
import { Component, Fragment, useEffect, useMemo, useState, type ReactNode } from 'react';
import type { ArchitectureOverviewDto } from '../core/intelligence-provider';
import type { GraphData, GraphNode } from '../galaxy/types';
import { graphNodeEvidence, useSelectionEvidence, type SelectionEvidenceListener } from '../galaxy/selection-evidence';
import { ArchitectureScene } from './ArchitectureScene';
import { buildSemanticGraph, semanticEntryPoints, type SemanticNode, type SemanticView } from './semantic-graph';
import { loadRouteGraph, type RouteGraphSnapshot } from './route-graph-source';
import { areaLevels } from './repository-map';
import { collectSourceMetrics, measureSourceNode } from './source-metrics';
import SourceMetricsDetails from './SourceMetricsDetails';
import type { CoverageIndex } from '../app/tree-model';
@@ -139,7 +140,7 @@ export default function SpatialArchitecture({ project, generation, graph, overvi
<div className="spatial-controls">
{['overview', 'dependencies', 'entryPoints'].includes(view) && <div className="spatial-projection-modes" role="group" aria-label="Overview mode"><button aria-pressed={view !== 'entryPoints'} onClick={() => onView('overview')}>Structure</button><button aria-pressed={view === 'entryPoints'} onClick={() => onView('entryPoints')}>Entry points</button></div>}
{(view === 'overview' || view === 'dependencies') && <nav aria-label="Architecture location"><button onClick={clearScope}>{project}</button>
{areaPath && <><span>/</span><button onClick={() => { setFilePath(undefined); setSelection(undefined); }}>{areaPath}</button></>}{filePath && <><span>/</span><span>{filePath.split('/').at(-1)}</span></>}
{areaPath && areaLevels(areaPath).map((level, index, levels) => <Fragment key={level}><span>/</span><button onClick={() => { setAreaPath(level); setFilePath(undefined); setSelection(undefined); }}>{index ? level.slice(levels[index - 1].length + 1) : level}</button></Fragment>)}{filePath && <><span>/</span><span>{filePath.split('/').at(-1)}</span></>}
</nav>}
{view === 'entryPoints' && <><label>Start <select aria-label="Entry point" value={currentEntry?.id ?? ''} onChange={event => { const node = entries.find(node => node.id === Number(event.target.value)); setEntryChoice(node ? { node, generation } : undefined); setSelection(undefined); }}>
{!entries.length && <option value="">No indexed entry points</option>}{entries.map(node => <option key={node.id} value={node.id}>{node.name} · {node.file_path}</option>)}
@@ -161,7 +162,8 @@ export default function SpatialArchitecture({ project, generation, graph, overvi
<div className="spatial-map-layout">
<div className="spatial-map"><SceneBoundary key={project}>
{model.nodes.length ? <ArchitectureScene model={model} selectedId={selectedNode?.id} selectedEdgeId={selectedEdge?.id} onSelect={selectNode}
onSelectEdge={id => { setSelection({ scope, edge: id }); setMemberLimit(5); }} onOpen={id => { const node = nodesById.get(id); if (node && (node.kind === 'area' || node.kind === 'file')) openGroup(node); }} onClearSelection={clearScope} active={active} planar={planar} resetKey={resetKey} catalog={catalog} heightMetric={heightMetric} colorMetric={colorMetric} hotspots={hotspotCatalog} showHotspots={showHotspots} />
onSelectEdge={id => { setSelection({ scope, edge: id }); setMemberLimit(5); }} onOpen={id => { const node = nodesById.get(id); if (node && (node.kind === 'area' || node.kind === 'file')) openGroup(node); }} onClearSelection={clearScope} active={active} planar={planar} resetKey={resetKey} catalog={catalog} heightMetric={heightMetric} colorMetric={colorMetric} hotspots={hotspotCatalog} showHotspots={showHotspots}
adaptiveLabels={view === 'overview' || view === 'dependencies'} />
: <div className="spatial-unavailable">{filter ? 'No matching graph evidence. Try a broader filter.' : view === 'hotspots' ? 'No ranked hotspot measurements are available in this snapshot.' : view === 'entryPoints' ? 'No indexed entry points in this snapshot.' : view === 'routes' ? 'No endpoint evidence is available in this snapshot.' : filePath ? 'No indexed symbols in this file. Use “Read this file” to inspect its source.' : 'No source nodes are available in this repository snapshot.'}</div>}
</SceneBoundary><div className="spatial-map-caption"><span title={model.positionMeaning}>{view === 'hotspots' ? 'Gravity: incoming references' : view === 'entryPoints' ? 'Static call paths' : 'Source folders and indexed relationships'}</span><span>Drag to orbit · scroll to zoom · select to inspect</span></div></div>
<aside className="spatial-inspector" aria-label="Architecture inspector">
@@ -21,6 +21,13 @@ describe('hotspot overlays and gravity', () => {
expect(hotspotsForNode(area, catalog)?.findings).toHaveLength(2);
expect(catalog.byFile.get('src/api/complex.ts')?.maxFanIn).toBeUndefined();
});
it('collects the findings of a sub-area inside an opened area', () => {
const nested: GraphData = { total_nodes: 2, edges: [], nodes: [node(1, 'busy', 'django/db/models/query.py'), node(2, 'complex', 'django/db/backends/base.py')] };
const catalog = collectHotspots(findings, nested);
const models = buildSemanticGraph(nested, { view: 'overview', areaPath: 'django/db' }).nodes.find(item => item.id === 'area:django/db/models')!;
expect(hotspotsForNode(models, catalog)).toMatchObject({ maxFanIn: 100, findings: [expect.objectContaining({ name: 'busy' })] });
expect(hotspotsForNode({ ...models, areaPath: 'django/views' }, catalog)).toBeUndefined();
});
it('does not turn complexity or missing fan-in into a gravity measurement', () => {
const catalog = collectHotspots(findings, graph);
expect(gravityStrength(catalog.byFile.get('src/api/complex.ts')!.maxFanIn, catalog.maxFanIn)).toBe(0);
+10 -2
View File
@@ -1,7 +1,7 @@
import type { ArchitectureHotspot } from '../core/intelligence-provider';
import type { GraphData, GraphNode } from '../galaxy/types';
import { layoutFolderHierarchy, pruneHierarchyPlatforms, type SemanticGraph, type SemanticNode } from './semantic-graph';
import { areaOf } from './repository-map';
import { areaOf, areaTrail } from './repository-map';
import { hotspotScore } from '../provider/cbm-rpc-provider';
export interface HotspotGroup { findings: ArchitectureHotspot[]; maxFanIn?: number; peakScore: number }
@@ -57,7 +57,15 @@ export function collectHotspots(findings: ArchitectureHotspot[], graph: GraphDat
}
export function hotspotsForNode(node: SemanticNode, catalog: HotspotCatalog): HotspotGroup | undefined {
if (node.kind === 'area') return node.areaPath ? catalog.byArea.get(node.areaPath) : undefined;
if (node.kind === 'area') {
const area = node.areaPath;
if (!area) return undefined;
// byArea holds the repository areas; the sub-areas of an opened area collect theirs by trail.
const repositoryArea = catalog.byArea.get(area);
if (repositoryArea) return repositoryArea;
const nested = catalog.findings.filter(finding => finding.filePath && areaTrail(finding.filePath).includes(area));
return nested.length ? summarize(nested) : undefined;
}
if (node.kind === 'file') return node.filePath ? catalog.byFile.get(node.filePath) : undefined;
return node.graphNode ? catalog.bySymbolId.get(node.graphNode.id) : undefined;
}
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { areaConnections, areaOf, repositoryMap } from './repository-map';
import { areaConnections, areaLevels, areaOf, areaTrail, repositoryMap } from './repository-map';
import type { GraphData, GraphNode } from '../galaxy/types';
export const node = (id: number, file_path: string, name = `symbol${id}`, status: GraphNode['status'] = 'normal'): GraphNode => ({
@@ -11,6 +11,26 @@ export const graph: GraphData = { total_nodes: 4, nodes: [node(1, 'src/api/serve
edges: [{ source: 1, target: 2, type: 'CALLS' }, { source: 2, target: 3, type: 'CALLS' },
{ source: 2, target: 3, type: 'IMPORTS' }, { source: 4, target: 2, type: 'CALLS' }] };
describe('area trails', () => {
it('repeats the repository area rule below each opened area', () => {
expect(areaTrail('django/contrib/admin/options.py')).toEqual(['django', 'django/contrib', 'django/contrib/admin']);
expect(areaTrail('django/shortcuts.py')).toEqual(['django']);
expect(areaTrail('graph-ui/src/app/AtlasChrome.tsx')).toEqual(['graph-ui', 'graph-ui/src/app']);
expect(areaTrail('graph-ui/src/App.tsx')).toEqual(['graph-ui', 'graph-ui/src']);
expect(areaTrail('src/mcp/mcp.c')).toEqual(['src/mcp']);
expect(areaTrail('Makefile')).toEqual(['(root)']);
for (const path of ['django/contrib/admin/options.py', 'graph-ui/src/app/AtlasChrome.tsx', 'src/mcp/mcp.c', 'Makefile']) {
expect(areaTrail(path)[0]).toBe(areaOf(path));
}
});
it('reconstructs the drill-in steps of an area from the area alone', () => {
expect(areaLevels('django/contrib/admin')).toEqual(['django', 'django/contrib', 'django/contrib/admin']);
expect(areaLevels('graph-ui/src')).toEqual(['graph-ui', 'graph-ui/src']);
expect(areaLevels('src/mcp')).toEqual(['src/mcp']);
expect(areaLevels('(root)')).toEqual(['(root)']);
});
});
describe('repository map evidence', () => {
it('retains exact directional edges and their source declarations across areas', () => {
const map = repositoryMap(graph);
@@ -33,6 +33,25 @@ export function areaOf(path: string): string {
return parts[0];
}
/**
* Every area a file belongs to, from the repository root inward. Each level
* applies areaOf to the path below the previous one, so an opened area shows
* its next folder level the same way the repository root shows its first.
* The last entry is the area the file sits in directly.
*/
export function areaTrail(path: string): string[] {
const trail = [areaOf(path)];
for (let area = trail[0]; area !== '(root)';) {
const inner = areaOf(path.slice(area.length + 1));
if (inner === '(root)') break;
area = `${area}/${inner}`; trail.push(area);
}
return trail;
}
/** The drill-in steps from the repository root down to an area taken from a trail. */
export const areaLevels = (area: string): string[] => area === '(root)' ? [area] : areaTrail(`${area}/-`);
const isSource = (node: GraphNode) => Boolean(node.file_path && node.file_path !== '{}'
&& !['Project', 'Folder', 'Package', 'Branch'].includes(node.label));
const priority = (a: GraphNode, b: GraphNode) => (b.in_calls ?? 0) - (a.in_calls ?? 0)
@@ -33,8 +33,11 @@ describe('source folder hierarchy layout', () => {
expect(filtered.platforms?.map(platform => platform.path)).toEqual(['src']);
filtered.nodes.forEach(item => expect(item.position).toEqual(full.nodes.find(original => original.id === item.id)!.position));
filtered.edges.forEach(edge => expect(edge).toEqual(full.edges.find(original => original.id === edge.id)));
// The cap applies before the layout, so a capped map frames only what it draws.
const capped = buildSemanticGraph(input, { view: 'overview', maxNodes: 1 });
expect(capped.nodes[0].position).toEqual(full.nodes.find(item => item.id === capped.nodes[0].id)!.position);
const cappedSource = capped.platforms!.find(platform => platform.path === 'src')!;
expect(containsNode(cappedSource, capped.nodes[0])).toBe(true);
expect(cappedSource.width * cappedSource.depth).toBeLessThan(source.width * source.depth);
const imports = buildSemanticGraph(input, { view: 'dependencies', relations: ['IMPORTS'] });
imports.nodes.forEach(item => expect(item.position).toEqual(full.nodes.find(original => original.id === item.id)!.position));
});
@@ -79,6 +82,70 @@ describe('source folder hierarchy layout', () => {
});
});
describe('drilling into large areas', () => {
// A Django-like top-level folder: 45 sub-packages, two direct files, and callers outside it.
const inside = Array.from({ length: 45 }, (_, index) => node(100 + index, `django/app${String(index).padStart(2, '0')}/models/base.py`, `model${index}`));
const large: GraphData = { total_nodes: 52, nodes: [...inside, node(200, 'django/shortcuts.py', 'render'), node(201, 'django/__init__.py', 'setup'),
node(202, 'tests/admin_views/tests.py', 'test_admin'), node(203, 'tests/runtests.py', 'run'), node(204, 'setup.py', 'install'),
node(205, 'django/app00/views/list.py', 'list_view'), node(206, 'django/app01/models/fields/json.py', 'JSONField')],
edges: [...inside.slice(1).map((item, index) => ({ source: item.id, target: inside[index].id, type: 'CALLS' })),
...inside.slice(0, 6).map(item => ({ source: 202, target: item.id, type: 'CALLS' })), { source: 203, target: 200, type: 'CALLS' },
...inside.slice(0, 4).map(item => ({ source: 204, target: item.id, type: 'IMPORTS' })), { source: 204, target: 201, type: 'IMPORTS' },
{ source: 205, target: 100, type: 'CALLS' }, { source: 206, target: 100, type: 'CALLS' }] };
const insidePlatform = (model: ReturnType<typeof buildSemanticGraph>) => model.platforms!.find(platform => platform.path === 'django')!;
it('opens a top-level folder at its next level, not at its files', () => {
expect(buildSemanticGraph(large, { view: 'overview' }).nodes.find(item => item.id === 'area:django')?.label).toBe('django');
const model = buildSemanticGraph(large, { view: 'overview', areaPath: 'django', maxNodes: 80 });
const inner = model.nodes.filter(item => !item.external);
expect(inner.filter(item => item.kind === 'area')).toHaveLength(45);
expect(inner.filter(item => item.kind === 'file').map(item => [item.id, item.label])).toEqual([
['file:django/__init__.py', '__init__.py'], ['file:django/shortcuts.py', 'shortcuts.py']]);
expect(model.nodes.find(item => item.id === 'area:django/app00')).toMatchObject({ label: 'app00', areaPath: 'django/app00', count: 2 });
expect(model.nodes.some(item => item.filePath?.startsWith('django/app'))).toBe(false);
expect(model.edges.find(edge => edge.source === 'area:django/app01' && edge.target === 'area:django/app00')?.count).toBe(2);
});
it('caps before the folder layout and keeps outside areas in their own lane', () => {
const model = buildSemanticGraph(large, { view: 'overview', areaPath: 'django' });
expect(model.nodes).toHaveLength(40);
expect(model.omittedNodes).toBe(model.totalNodes - 40);
const outside = model.nodes.filter(item => item.external);
expect(outside.map(item => item.id).sort()).toEqual(['area:(root)', 'area:tests']);
outside.forEach(item => expect(item.detail).toBe('External source area · inferred from file paths'));
const platform = insidePlatform(model);
const lane = model.platforms!.find(item => item.id === 'hierarchy:outside')!;
expect(lane.label).toBe('Outside');
expect(model.platforms!.map(item => item.id).sort()).toEqual(['hierarchy:folder:django', 'hierarchy:outside']);
model.nodes.filter(item => !item.external).forEach(item => expect(containsNode(platform, item)).toBe(true));
outside.forEach(item => { expect(containsNode(lane, item)).toBe(true); expect(containsNode(platform, item)).toBe(false); });
// The platform is exactly the packing of the drawn bricks, not of all 47 inner parts.
const drawn = model.nodes.filter(item => !item.external).map(item => ({ ...item, position: [0, 0, 0] as [number, number, number] }));
const repacked = layoutFolderHierarchy(drawn, 'django').find(item => item.path === 'django')!;
expect([platform.width, platform.depth]).toEqual([repacked.width, repacked.depth]);
const complete = insidePlatform(buildSemanticGraph(large, { view: 'overview', areaPath: 'django', maxNodes: 80 }));
expect(platform.width * platform.depth).toBeLessThan(complete.width * complete.depth);
});
it('names outside areas as siblings of a nested area and keeps relative labels below it', () => {
const nested = buildSemanticGraph(large, { view: 'overview', areaPath: 'django/app00' });
expect(nested.title).toBe('django/app00');
expect(nested.nodes.filter(item => !item.external).map(item => [item.id, item.label])).toEqual([
['area:django/app00/models', 'models'], ['area:django/app00/views', 'views']]);
expect(nested.nodes.filter(item => item.external).map(item => item.id).sort()).toEqual(['area:(root)', 'area:django/app01', 'area:tests']);
expect(nested.platforms!.find(item => item.path === 'django/app00')?.label).toBe('django/app00');
const deeper = buildSemanticGraph(large, { view: 'overview', areaPath: 'django/app01/models' });
expect(deeper.nodes.filter(item => !item.external).map(item => item.label)).toEqual(['fields', 'base.py']);
expect(deeper.platforms!.filter(item => item.id !== 'hierarchy:outside').map(item => item.path)).toEqual(['django/app01/models']);
});
it('applies the source-container rule relative to the opened area', () => {
const ui: GraphData = { total_nodes: 3, edges: [], nodes: [node(1, 'graph-ui/src/App.tsx', 'App'),
node(2, 'graph-ui/src/app/AtlasChrome.tsx', 'AtlasChrome'), node(3, 'graph-ui/vite.config.ts', 'config')] };
const model = buildSemanticGraph(ui, { view: 'overview', areaPath: 'graph-ui' });
expect(model.nodes.map(item => [item.id, item.label])).toEqual([['area:graph-ui/src', 'src'], ['area:graph-ui/src/app', 'src/app'],
['file:graph-ui/vite.config.ts', 'vite.config.ts']]);
expect(buildSemanticGraph(ui, { view: 'overview', areaPath: 'graph-ui/src' }).nodes.map(item => item.id)).toEqual(['file:graph-ui/src/App.tsx']);
});
});
describe('semantic architecture projection', () => {
it('represents disconnected and inventory-only files without inventing graph evidence', () => {
const graph: GraphData = { nodes: [node(1, 'src/api/main.ts'), { ...node(2, 'isolated/empty.ts'), label: 'File' }], edges: [], total_nodes: 2 };
+89 -35
View File
@@ -1,6 +1,6 @@
import type { RouteRef } from '../core/intelligence-provider';
import type { GraphData, GraphEdge, GraphNode } from '../galaxy/types';
import { areaOf, MAP_RELATIONS } from './repository-map';
import { areaLevels, areaOf, areaTrail, MAP_RELATIONS } from './repository-map';
import type { RouteGraphSnapshot } from './route-graph-source';
export type SemanticView = 'overview' | 'dependencies' | 'entryPoints' | 'routes' | 'hotspots';
@@ -22,6 +22,8 @@ export interface SemanticNode {
footprint?: [number, number];
tint?: string;
kindLabel?: string;
/** An area outside the opened area or file; it keeps to its own lane. */
external?: boolean;
}
export interface SemanticEvidence {
source: GraphNode;
@@ -181,11 +183,12 @@ function packHierarchy(box: HierarchyBox): void {
}
/**
* Arrange unfiltered candidates by actual source folders, never relation count
* or hotspot rank. Only positions change; callers retain all graph identities.
* Deep paths share their third real ancestor to keep the steps shallow.
* Arrange candidates by actual source folders, never relation count or hotspot
* rank. Only positions change; callers retain all graph identities. Deep paths
* share their third real ancestor to keep the steps shallow; an opened area
* passes itself as `base` and counts as the first of those steps.
*/
export function layoutFolderHierarchy(nodes: SemanticNode[]): SemanticPlatform[] {
export function layoutFolderHierarchy(nodes: SemanticNode[], base = ''): SemanticPlatform[] {
if (!nodes.length) return [];
const root: HierarchyBox = { key: '', path: '', level: 0, children: [], width: 0, depth: 0, x: 0, z: 0 };
const folders = new Map<string, HierarchyBox>();
@@ -208,7 +211,8 @@ export function layoutFolderHierarchy(nodes: SemanticNode[]): SemanticPlatform[]
}
owner = unknown;
} else {
const parts = containing.split('/').filter(Boolean).slice(0, 3);
const parts = (base && (containing === base || containing.startsWith(`${base}/`))
? [base, ...containing.slice(base.length + 1).split('/')] : containing.split('/')).filter(Boolean).slice(0, 3);
parts.forEach((_, index) => {
const folderPath = parts.slice(0, index + 1).join('/');
const key = `folder:${folderPath}`;
@@ -240,6 +244,28 @@ export function layoutFolderHierarchy(nodes: SemanticNode[]): SemanticPlatform[]
return platforms;
}
/**
* Areas outside the opened area or file wait in a lane in front of its
* platforms, so the platforms hold only what lies inside.
*/
function placeOutside(outside: SemanticNode[], inside: SemanticNode[], platforms: SemanticPlatform[]): SemanticPlatform | undefined {
if (!outside.length) return undefined;
const spacing = 34;
const extents = [...platforms.map(platform => [platform.position[0] - platform.width / 2, platform.position[0] + platform.width / 2, platform.position[2] + platform.depth / 2]),
...inside.map(node => [node.position[0] - 12, node.position[0] + 12, node.position[2] + 12])];
const left = extents.length ? Math.min(...extents.map(extent => extent[0])) : 0;
const right = extents.length ? Math.max(...extents.map(extent => extent[1])) : 0;
const top = (extents.length ? Math.max(...extents.map(extent => extent[2])) : 0) + 14;
const columns = Math.max(1, Math.min(outside.length, Math.max(Math.floor((right - left + 10) / spacing), Math.ceil(Math.sqrt(outside.length)))));
const rows = Math.ceil(outside.length / columns);
// One extra column on the left keeps the lane's own label clear of the first brick label.
const width = (columns + 1) * spacing + 14; const depth = rows * spacing + 18; const center = (left + right) / 2;
outside.forEach((node, index) => {
node.position = [center + (index % columns + 1 - columns / 2) * spacing, 1.68, top + 28 + Math.floor(index / columns) * spacing];
});
return { id: 'hierarchy:outside', path: '', label: 'Outside', position: [center, 1.6, top + depth / 2], width, depth, level: 1 };
}
/** Prune empty containers without repacking the stable unfiltered layout. */
export function pruneHierarchyPlatforms(platforms: SemanticPlatform[], nodes: SemanticNode[]): SemanticPlatform[] {
return platforms.filter(platform => nodes.some(node => node.position[1] >= platform.position[1]
@@ -263,7 +289,7 @@ export function buildSemanticGraph(graph: GraphData, options: SemanticGraphOptio
let edges: SemanticEdge[] = [];
const warnings: string[] = [];
let title = options.filePath ?? options.areaPath ?? 'Repository structure';
let positionMeaning = 'Nested platforms follow source folders. Positions are stable across text filters; boxes represent source grouping, not deployed services.';
let positionMeaning = 'Nested platforms follow source folders; areas outside the opened one wait in their own lane. Boxes represent source grouping, not deployed services.';
const depth = bounded(options.depth, 3, 8);
let rootId: string | undefined;
@@ -350,21 +376,29 @@ export function buildSemanticGraph(graph: GraphData, options: SemanticGraphOptio
} else {
const groups = new Map<string, SemanticNode>();
const nodeGroups = new Map<number, string>();
const trails = new Map<string, string[]>();
const trailOf = (path: string) => { let trail = trails.get(path); if (!trail) trails.set(path, trail = areaTrail(path)); return trail; };
// An opened area shows its next level: deeper areas, and the files that sit in it directly.
const scope = options.filePath ? undefined : options.areaPath;
const levelOf = (path: string): { kind: 'area' | 'file'; path: string } | undefined => {
const trail = trailOf(path);
if (!scope) return { kind: 'area', path: trail[0] };
const at = trail.indexOf(scope);
return at < 0 ? undefined : at + 1 < trail.length ? { kind: 'area', path: trail[at + 1] } : { kind: 'file', path };
};
const scopedLabel = (path: string) => scope && path.startsWith(`${scope}/`) ? path.slice(scope.length + 1) : path;
for (const node of graph.nodes.filter(sourceNode).sort(nodeOrder)) {
if (options.visibleFiles && !options.visibleFiles.has(node.file_path!)) continue;
const area = areaOf(node.file_path!);
if (options.areaPath && area !== options.areaPath) continue;
if (options.filePath && node.file_path !== options.filePath) continue;
if (options.filePath) {
if (['File', 'Module'].includes(node.label)) continue;
if (node.file_path !== options.filePath || ['File', 'Module'].includes(node.label)) continue;
const result = symbolNode(node); groups.set(result.id, result); nodeGroups.set(node.id, result.id);
continue;
}
const kind = options.areaPath ? 'file' : 'area';
const path = options.areaPath ? node.file_path! : area;
const id = `${kind}:${path}`;
const group = groups.get(id) ?? { id, kind, label: path, detail: '', position: [0, 0, 0], count: 0,
filePath: kind === 'file' ? path : undefined, areaPath: area, members: [] };
const level = levelOf(node.file_path!);
if (!level) continue;
const id = `${level.kind}:${level.path}`;
const group = groups.get(id) ?? { id, kind: level.kind, label: scopedLabel(level.path), detail: '', position: [0, 0, 0], count: 0,
filePath: level.kind === 'file' ? level.path : undefined, areaPath: level.kind === 'area' ? level.path : scope, members: [] };
group.members.push(node); group.count++; groups.set(id, group); nodeGroups.set(node.id, id);
}
// Retain files with no graph nodes/edges as navigable inventory objects.
@@ -372,23 +406,30 @@ export function buildSemanticGraph(graph: GraphData, options: SemanticGraphOptio
const inventory = new Map<string, Set<string>>();
for (const path of options.knownFiles ?? []) {
if (!path || path === '{}' || (options.visibleFiles && !options.visibleFiles.has(path))) continue;
const area = areaOf(path);
if ((options.areaPath && area !== options.areaPath) || (options.filePath && path !== options.filePath)) continue;
const kind = options.areaPath || options.filePath ? 'file' : 'area';
const key = kind === 'file' ? path : area;
const id = `${kind}:${key}`;
if (options.filePath && path !== options.filePath) continue;
const level = options.filePath ? { kind: 'file' as const, path } : levelOf(path);
if (!level) continue;
const id = `${level.kind}:${level.path}`;
const paths = inventory.get(id) ?? new Set<string>(); paths.add(path); inventory.set(id, paths);
if (options.filePath && [...groups.values()].some(group => group.filePath === path)) continue;
if (!groups.has(id)) groups.set(id, { id, kind, label: key,
if (!groups.has(id)) groups.set(id, { id, kind: level.kind, label: options.filePath ? path : scopedLabel(level.path),
detail: options.filePath ? 'File inventory · no symbols in the loaded graph' : '',
position: [0, 0, 0], count: 0, areaPath: area, filePath: kind === 'file' ? path : undefined, members: [] });
position: [0, 0, 0], count: 0, areaPath: level.kind === 'area' ? level.path : scope ?? areaOf(path),
filePath: level.kind === 'file' ? path : undefined, members: [] });
}
let scopedEvidence = evidence;
if (options.areaPath || options.filePath) {
const inScope = (node: GraphNode) => sourceNode(node)
&& (!options.visibleFiles || options.visibleFiles.has(node.file_path!))
&& (!options.areaPath || areaOf(node.file_path!) === options.areaPath)
&& (!options.filePath || node.file_path === options.filePath);
&& (options.filePath ? node.file_path === options.filePath : levelOf(node.file_path!) !== undefined);
// An outside area is named where its trail leaves the opened one: a sibling, not a repository root area.
const reference = scope ? areaLevels(scope) : trailOf(options.filePath!);
const outsideArea = (path: string) => {
const trail = trailOf(path);
let shared = 0;
while (shared < trail.length && trail[shared] === reference[shared]) shared++;
return trail[Math.min(shared, trail.length - 1)];
};
scopedEvidence = evidence.filter(edge => inScope(edge.source) || inScope(edge.target));
for (const edge of scopedEvidence) {
if (inScope(edge.source) === inScope(edge.target)) continue;
@@ -407,11 +448,11 @@ export function buildSemanticGraph(graph: GraphData, options: SemanticGraphOptio
groups.set(id, group); nodeGroups.set(inside.id, id);
}
if (!nodeGroups.has(inside.id)) continue;
const areaPath = areaOf(outside.file_path!);
const areaPath = outsideArea(outside.file_path!);
const id = `area:${areaPath}`;
const group: SemanticNode = groups.get(id) ?? { id, kind: 'area', label: areaPath,
detail: 'External source area · inferred from file paths', position: [0, 0, 0],
count: 0, areaPath, members: [] };
count: 0, areaPath, members: [], external: true };
if (!group.members.includes(outside)) { group.members.push(outside); group.count++; }
groups.set(id, group); nodeGroups.set(outside.id, id);
}
@@ -422,12 +463,20 @@ export function buildSemanticGraph(graph: GraphData, options: SemanticGraphOptio
if (options.view === 'dependencies') title = options.filePath ?? options.areaPath ?? 'Dependencies between source areas';
}
const platforms = options.view !== 'entryPoints' && options.view !== 'routes' ? layoutFolderHierarchy(nodes) : undefined;
const filter = options.filter?.trim().toLowerCase();
const matching = new Set<string>();
const capped = (candidates: SemanticNode[], relations: SemanticEdge[]) => {
const degrees = new Map<string, number>();
for (const edge of relations) for (const id of [edge.source, edge.target]) degrees.set(id, (degrees.get(id) ?? 0) + edge.count);
return [...candidates].sort((a, b) => (a.id === rootId ? -1 : b.id === rootId ? 1 : 0)
|| Number(matching.has(b.id)) - Number(matching.has(a.id))
|| (a.depth ?? 0) - (b.depth ?? 0) || (degrees.get(b.id) ?? 0) - (degrees.get(a.id) ?? 0) || compare(a.id, b.id))
.slice(0, bounded(options.maxNodes, 40, 80)).sort((a, b) => compare(a.id, b.id));
};
const unfiltered = capped(nodes, edges);
if (filter) {
for (const node of nodes) {
if (`${node.label} ${node.detail} ${node.members.map(member => member.name).join(' ')}`.toLowerCase().includes(filter)) matching.add(node.id);
if (`${node.label} ${node.filePath ?? node.areaPath ?? ''} ${node.detail} ${node.members.map(member => member.name).join(' ')}`.toLowerCase().includes(filter)) matching.add(node.id);
}
const context = new Set(matching);
for (const edge of edges) {
@@ -438,12 +487,17 @@ export function buildSemanticGraph(graph: GraphData, options: SemanticGraphOptio
if (context.size > matching.size) warnings.push('Showing filter matches and their one-hop neighbors for relationship context.');
}
const totalNodes = nodes.length; const totalEdges = edges.length;
const degrees = new Map<string, number>();
for (const edge of edges) for (const id of [edge.source, edge.target]) degrees.set(id, (degrees.get(id) ?? 0) + edge.count);
nodes.sort((a, b) => (a.id === rootId ? -1 : b.id === rootId ? 1 : 0)
|| Number(matching.has(b.id)) - Number(matching.has(a.id))
|| (a.depth ?? 0) - (b.depth ?? 0) || (degrees.get(b.id) ?? 0) - (degrees.get(a.id) ?? 0) || compare(a.id, b.id));
nodes = nodes.slice(0, bounded(options.maxNodes, 40, 80)).sort((a, b) => compare(a.id, b.id));
nodes = filter ? capped(nodes, edges) : unfiltered;
// Cap first, then lay out, so the platforms are sized to what is drawn. A
// filter that only narrows the drawn map keeps its positions.
let platforms: SemanticPlatform[] | undefined;
if (options.view !== 'entryPoints' && options.view !== 'routes') {
const layout = nodes.every(node => unfiltered.includes(node)) ? unfiltered : nodes;
const inside = layout.filter(node => !node.external);
platforms = layoutFolderHierarchy(inside, options.filePath ? '' : options.areaPath ?? '');
const lane = placeOutside(layout.filter(node => node.external), inside, platforms);
if (lane) platforms.push(lane);
}
const visibleIds = new Set(nodes.map(node => node.id));
edges = edges.filter(edge => visibleIds.has(edge.source) && visibleIds.has(edge.target))
.sort((a, b) => Number(matching.has(b.source) || matching.has(b.target)) - Number(matching.has(a.source) || matching.has(a.target))
@@ -49,6 +49,17 @@ describe('source size and language encoding', () => {
expect(catalog.referencePath).toBe('src/store');
expect(languageColor('TypeScript')).toBe(languageColor(sourceLanguage('other/project/test.tsx')));
});
it('measures the sub-areas of an opened area without changing the repository reference', () => {
const nested: GraphData = { total_nodes: 3, edges: [], nodes: [node(1, 'django/contrib/admin/options.py', 'Module', 1, 400),
node(2, 'django/contrib/auth/models.py', 'Module', 1, 100), node(3, 'django/shortcuts.py', 'Module', 1, 50)] };
const catalog = collectSourceMetrics(nested);
expect(catalog.areas.get('django')?.lines).toBe(550);
expect(catalog.areas.get('django/contrib')?.lines).toBe(500);
expect(catalog.areas.get('django/contrib/admin')?.lines).toBe(400);
expect(catalog.referencePath).toBe('django');
const area = buildSemanticGraph(nested, { view: 'overview', areaPath: 'django' }).nodes.find(item => item.id === 'area:django/contrib')!;
expect(measureSourceNode(area, catalog)?.lines).toBe(500);
});
it('compresses million-line outliers into finite bounded monotonic heights', () => {
const heights = [0, 1, 100, 10000, 1000000].map(lines => sourceBrickHeight(lines, 1000000));
heights.forEach((height, index) => { expect(Number.isFinite(height)).toBe(true); expect(height).toBeGreaterThanOrEqual(1.5); expect(height).toBeLessThanOrEqual(14); if (index) expect(height).toBeGreaterThan(heights[index - 1]); });
+6 -4
View File
@@ -1,6 +1,6 @@
import type { GraphData } from '../galaxy/types';
import type { SemanticNode } from './semantic-graph';
import { areaOf, MAP_RELATIONS } from './repository-map';
import { areaTrail, MAP_RELATIONS } from './repository-map';
export interface LanguageMeasure { name: string; color: string; lines: number; files: number }
export interface SourceMeasure {
@@ -102,9 +102,10 @@ export function collectSourceMetrics(graph: GraphData, knownFiles: string[] = []
if (file) file.connection = 'connected';
}
}
// Every level of a file's trail, so an opened area can measure its own sub-areas.
const areaFiles = new Map<string, FileMeasure[]>();
for (const file of files.values()) {
const area = areaOf(file.path); const items = areaFiles.get(area) ?? [];
for (const file of files.values()) for (const area of areaTrail(file.path)) {
const items = areaFiles.get(area) ?? [];
items.push(file); areaFiles.set(area, items);
}
const areas = new Map([...areaFiles].map(([area, members]) => [area, summarize(members)]));
@@ -118,7 +119,8 @@ export function collectSourceMetrics(graph: GraphData, knownFiles: string[] = []
return result;
};
// Root-direct files remain truthful measurements, but the root platform is
// the visual baseline rather than a competing folder-size denominator.
// the visual baseline rather than a competing folder-size denominator. A
// nested area never outgrows the repository area that contains it.
const folderReference = largest([...areas].filter(([path]) => path !== '(root)').map(([path, area]) => [path, area.lines]));
const reference = folderReference ?? largest([...files.values()].map(file => [file.path, file.lines]));
return { files, areas, total, referenceLines: Math.max(1, reference?.lines ?? 1),