feat(ui): the Map — the repository at module granularity, layered from the graph (CG-49)

`GET /api/map` rolls the whole edge table up to module granularity in one
`GROUP BY`, and the Map tab draws it: one box per directory, dependencies
pointing down, nothing placed by hand.

Two decisions carry the screen.

The vertical order rests on each link's `declared` weight — the edges resolved
through an import, a qualified name, an inheritance clause or a typed receiver —
not on its raw count. Bare name matching resolves `run`, `push` and `finish`
across unrelated directories, and layering on raw counts put `src/db` directly
under `src/bin` on this repository's own index. On declared edges the same data
reproduces the pipeline CLAUDE.md describes, with a third of the mutual pairs.
When too few links carry a declared edge to describe a project, the layout falls
back to raw counts and the side panel says so.

And the aggregation is a single scan. Grouping by the symbol names as well as
the modules costs nothing extra — the join is what is expensive — so one query
yields both the link weights and the tooltip's symbol pairs. Measured against
this index inflated to 800k edges: 1.28s for one scan against 1.89s for two,
which is the difference between meeting and missing the cold budget on a
ten-thousand-file repository. Cached answers come back in ~3ms.

Nothing is dropped silently: thin links are hidden until a module they touch is
selected and counted in the panel, uncertain references are excluded from every
number on screen and the total is printed, and mutual dependencies, module loops
and file-level circular imports are listed rather than straightened away. An
edge that still points up after layering is drawn dashed on selection instead of
being reversed or removed.

The layout — cycle-breaking, longest-path layering, barycenter ordering, ports —
is a pure function of the payload in `ui/src/lib/map-model.ts`, so the tests
toggle and the selection cost no round-trip and the same project always draws
the same picture. Svelte Flow supplies pan, zoom and fit; never a layout.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Colby McHenry
2026-08-27 02:38:24 -05:00
co-authored by Claude Opus 5
parent a1dfa72cac
commit 6d0f60f32c
19 changed files with 3501 additions and 21 deletions
+29
View File
@@ -1400,6 +1400,35 @@ export class CodeGraph {
);
}
/**
* Roll the edge table up to module granularity, for a file → module
* assignment the caller decides.
*
* The architecture map's single query: cross-module edge counts by kind,
* the `declared` subset of each (see {@link QueryBuilder.aggregateModuleGraph}),
* and the busiest symbol pairs behind each link. Read-only, and bounded by
* the number of modules rather than the number of edges.
*/
getModuleAggregation(
assignments: ReadonlyArray<{ filePath: string; module: string }>,
options: {
kinds: readonly Edge['kind'][];
minConfidence: number;
topPairsPerLink: number;
pairKinds: readonly Edge['kind'][];
}
): ReturnType<QueryBuilder['aggregateModuleGraph']> {
return this.queries.aggregateModuleGraph(assignments, options);
}
/**
* Every ordered pair of files where one reaches into the other — the edge
* list a cycle finder runs on. See {@link QueryBuilder.getCrossFileDependencyPairs}.
*/
getFileDependencyPairs(minConfidence = 0): Array<{ source: string; target: string }> {
return this.queries.getCrossFileDependencyPairs(minConfidence);
}
/**
* References from a symbol that never resolved to an indexed node — the
* calls and type mentions that leave the index. Lets a reader account for