fix(ui): show the part that tells similar route labels apart

A compact route chip shows about fourteen characters. After "Show
these 2 routes" both chips read "/generic-lastmod...", and unrelated
routes such as /callable-lastmod-full-sitemap.xml and
/callable-lastmod-partial-sitemap.xml were cut to the same text too.
Labels that would read alike now drop their shared start at a
separator ("...index.xml" style, a slash kept for path segments); the
tooltip, the inspector and the list keep the full path.

Signed-off-by: Bernhard Jackiewicz <bj@techport.io>
This commit is contained in:
Bernhard Jackiewicz
2026-10-03 19:02:00 +02:00
parent 127db22acf
commit 1999aaba8a
3 changed files with 51 additions and 2 deletions
@@ -173,7 +173,7 @@ function ArchitectureNode({ node, selected, highlighted, dimmed, labelVisible =
onPointerEnter={() => onHover(node.id)} onPointerLeave={() => onHover()}
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>
<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.shortLabel ?? 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>}
{selected && <span className="architecture-node-detail">{node.detail}</span>}
</button>
@@ -214,7 +214,7 @@ function HotspotLabels({ model, priorityId, onVisible }: { model: RenderGraph; p
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 box = boxes.current.get(node.id);
const width = box?.width ?? Math.min(178, Math.max(78, node.label.length * 7 + (node.gravityPercent === undefined ? 26 : 68)));
const width = box?.width ?? Math.min(178, Math.max(78, (node.shortLabel ?? node.label).length * 7 + (node.gravityPercent === undefined ? 26 : 68)));
const height = node.id === priorityId ? 96 : box?.height ?? 38;
const rect = labelRect(x, y, width, height, box ? inset : [-5, 0]);
if (node.id !== priorityId && (rect.right < 0 || rect.left > size.width || rect.bottom < 0 || rect.top > size.height
@@ -332,6 +332,22 @@ describe('route identities and service evidence', () => {
const filtered = buildSemanticGraph(graph, { view: 'routes', groupRoutes: true, filter: '/accounts', routeSnapshot });
expect(filtered.nodes.filter(item => item.kind === 'route').map(item => item.label)).toEqual(['/accounts/login/', '/accounts/logout/']);
});
it('shortens the shared start of routes that would read alike and keeps the distinguishing part', () => {
const paths = ['/generic-lastmod/index.xml', '/generic-lastmod/sitemap.xml', '/callable-lastmod-full-sitemap.xml', '/callable-lastmod-partial-sitemap.xml', '/about/'];
const routes = paths.map((name, index) => ({ ...route, id: 600 + index, name, qualified_name: `fixture.route.${600 + index}`, file_path: 'tests/sitemaps_tests/urls/http.py' }));
const graph: GraphData = { nodes: routes, edges: [], total_nodes: routes.length };
const shown = (model: ReturnType<typeof buildSemanticGraph>) => Object.fromEntries(model.nodes.filter(item => item.kind === 'route')
.map(item => [item.label, item.shortLabel ?? item.label]));
// "Show these 2 routes" filters by the group prefix: both labels started "/generic-lastmod…" and read alike.
expect(shown(buildSemanticGraph(graph, { view: 'routes', groupRoutes: true, filter: '/generic-lastmod' })))
.toEqual({ '/generic-lastmod/index.xml': '…/index.xml', '/generic-lastmod/sitemap.xml': '…/sitemap.xml' });
// Distinct first segments are not grouped, yet were cut to the same "/callable-lastmo…".
const grouped = shown(buildSemanticGraph(graph, { view: 'routes', groupRoutes: true }));
expect(grouped['/callable-lastmod-full-sitemap.xml']).toBe('…full-sitemap.xml');
expect(grouped['/callable-lastmod-partial-sitemap.xml']).toBe('…partial-sitemap.xml');
expect(grouped['/about/']).toBe('/about/');
expect(grouped['/generic-lastmod · 2']).toBe('/generic-lastmod · 2');
});
it('selects exactly the routes of a group when the filter names its prefix', () => {
const at = (id: number, name: string, file_path: string): GraphNode => ({ ...route, id, name, qualified_name: `fixture.route.${id}`, file_path });
const routes = [at(400, '/', 'app/urls.py'), at(401, '/', 'shop/urls.py'), at(402, '/', 'blog/urls.py'),
@@ -26,6 +26,8 @@ export interface SemanticNode {
external?: boolean;
/** The first path segment shared by the routes of a route group. */
routePrefix?: string;
/** What the chip shows when the full label would read like a neighbour's; the tooltip keeps the full label. */
shortLabel?: string;
}
export interface SemanticEvidence {
source: GraphNode;
@@ -110,6 +112,33 @@ const TEST_SOURCE = /(^|\/)(tests|__tests__|specs?)\/|^(src\/)?test\/|(^|\/)(tes
* (decodeURI), and a path that is not valid percent-encoding stays as indexed. */
export const readableRoute = (path: string): string => { try { return decodeURI(path); } catch { return path; } };
const routePrefixOf = (label: string) => `/${label.replace(/^[A-Z]+\s+/, '').split('/').filter(Boolean)[0] ?? ''}`;
/** Characters a compact route chip shows before it cuts the label (spatial-architecture.css, 112px at 10px). */
const COMPACT_LABEL_CHARS = 14;
/**
* Labels that a compact chip would cut to the same visible start ("/generic-lastmo…")
* lose their shared start instead: "…/index.xml" and "…/sitemap.xml". The cut falls on a
* separator, so the rest begins with a whole segment or word; a slash stays to mark a path.
*/
export function distinctLabels(labels: readonly string[], visible = COMPACT_LABEL_CHARS): Map<string, string> {
const result = new Map<string, string>();
const long = [...new Set(labels)].filter(label => label.length > visible);
for (const label of long) {
let shared = 0;
for (const other of long) {
if (other === label || other.slice(0, visible - 1) !== label.slice(0, visible - 1)) continue;
let common = 0;
while (common < label.length && label[common] === other[common]) common++;
shared = Math.max(shared, common);
}
if (!shared) continue;
const cut = Math.max(...['/', '-', '_', '.'].map(separator => label.lastIndexOf(separator, shared - 1)));
const rest = cut > 0 ? (label[cut] === '/' ? label.slice(cut) : label.slice(cut + 1)) : '';
// A label that is the shared start itself keeps its full text; its neighbours carry the difference.
if (rest.replace('/', '').length > 1) result.set(label, `…${rest}`);
}
return result;
}
const sourceNode = (node: GraphNode) => Boolean(node.file_path && node.file_path !== '{}'
&& !['Project', 'Folder', 'Package', 'Branch', 'Route'].includes(node.label));
const symbolNode = (node: GraphNode): SemanticNode => ({ id: symbolId(node),
@@ -601,6 +630,10 @@ export function buildSemanticGraph(graph: GraphData, options: SemanticGraphOptio
options.view === 'entryPoints' ? lane * 3 : 0, (siblingIndex % laneRows - (laneRows - 1) / 2) * 20];
}
});
if (options.view === 'routes') {
const short = distinctLabels(nodes.filter(node => node.kind === 'route').map(node => node.label));
for (const node of nodes) if (node.kind === 'route' && short.has(node.label)) node.shortLabel = short.get(node.label);
}
if (unresolved) warnings.push(`${unresolved} relationships have an endpoint outside the loaded repository snapshot.`);
if (graph.total_nodes > graph.nodes.length) warnings.push('The loaded repository snapshot contains only part of the indexed graph.');
return { view: options.view, scopeKey: `${options.view}:${options.areaPath ?? ''}:${options.filePath ?? ''}:${rootId ?? ''}:${filter ?? ''}`,