feat(ui): entry points — routes, executable files and tests as flow starting points (CG-54)
`#/entry` answers "where does anything start" at full length, and turns any row that names a symbol into a flow. Server. `/api/entrypoints` gains `frameworks` (from `getDetectedFrameworks`), a `tests` list, a `routes` limit of its own, and a cache keyed on the index build — nothing here is read from disk, so unlike `/api/source` a cached answer cannot be stale about drift. `routes.items` is now a `WireList` like every other list on the payload. Routes carry where the URL is REGISTERED as well as where it is served: `getRoutingManifest` selects the route node's id, file and line, and `buildRoutes` splits the verb off the name against a fixed list (never "the first word", which would take the head off a file-routed `/blog/[slug]`). All four payroll-go routes register in one router file and three are served from another — group by the handler file and one router becomes two groups plus an orphan. `isTestFile` is split into `isTestPath` (test filename and directory conventions) + the non-production catch-all, byte-identical at every existing call site. The Tests list uses the narrow half: an example, a benchmark or a fixture is off-target for ranking but is not a test, and a heading that says "Tests" must not quietly count them. Tests rank by REACH — distinct other files touched — because Go, Rust and Java put test work inside functions where a module-level-calls ranking sees nothing. Two read-only engine queries make that affordable: `getFileReachCounts` (the mirror of `getFileDependentCounts`, driven from `nodes` by path so the cost follows the files asked about rather than the edge table) and `getFileNodes`. Viewer. `ui/src/lib/entry-model.ts` folds the four lists into file groups — pure, and `panel.rows` stays exactly the sections it draws. `EntryView` + `EntrySection` render them with the caller rail's `.filegroup` / `.row` shapes rather than a second visual language for the same idea. A row that names a callable symbol carries a `Flow ›` chip; the other end is typed or picked with `→ here` on another row. File and test rows carry none: `/api/flow` searches by name, and a file has none the path finder can look up. A project with fewer than three resolvable routes gets no Routes heading at all, not an empty one. Typing into the search box now also returns matching entry points under their own heading below the symbol matches, so a URL comes back with its handler attached; rows already in the results are dropped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
dc7f1e590e
commit
94f4e287e6
@@ -186,7 +186,15 @@ describe('the palette', () => {
|
||||
|
||||
function entryPoints(over: Partial<WireEntryPoints> = {}): WireEntryPoints {
|
||||
return {
|
||||
routes: { routed: false, routeCount: 0, items: [] },
|
||||
frameworks: [],
|
||||
routes: {
|
||||
routed: false,
|
||||
routeCount: 0,
|
||||
items: { total: 0, shown: 0, truncated: false, items: [] },
|
||||
},
|
||||
tests: { total: 0, shown: 0, truncated: false, items: [] },
|
||||
index: { lastIndexedAt: null, files: 0 },
|
||||
timing: { elapsedMs: 0, cached: false },
|
||||
files: {
|
||||
total: 2,
|
||||
shown: 2,
|
||||
@@ -232,15 +240,26 @@ describe('the entry points', () => {
|
||||
routes: {
|
||||
routed: true,
|
||||
routeCount: 4,
|
||||
items: [
|
||||
{
|
||||
url: 'GET /users',
|
||||
handler: 'listUsers',
|
||||
file: 'src/routes.ts',
|
||||
line: 11,
|
||||
handlerId: 'function:listUsers',
|
||||
},
|
||||
],
|
||||
items: {
|
||||
total: 1,
|
||||
shown: 1,
|
||||
truncated: false,
|
||||
items: [
|
||||
{
|
||||
url: 'GET /users',
|
||||
method: 'GET',
|
||||
path: '/users',
|
||||
handler: 'listUsers',
|
||||
handlerKind: 'function',
|
||||
file: 'src/routes.ts',
|
||||
line: 11,
|
||||
handlerId: 'function:listUsers',
|
||||
routeFile: 'src/routes.ts',
|
||||
routeLine: 4,
|
||||
routeId: 'route:src/routes.ts:4:GET:/users',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
@@ -265,6 +284,67 @@ describe('the entry points', () => {
|
||||
expect(buildEntryPalette(many).items).toHaveLength(11);
|
||||
});
|
||||
|
||||
it('offers entry points under a typed query, BELOW the symbol matches', () => {
|
||||
const entries = entryPoints({
|
||||
routes: {
|
||||
routed: true,
|
||||
routeCount: 3,
|
||||
items: {
|
||||
total: 1,
|
||||
shown: 1,
|
||||
truncated: false,
|
||||
items: [
|
||||
{
|
||||
url: 'POST /users',
|
||||
method: 'POST',
|
||||
path: '/users',
|
||||
handler: 'createUser',
|
||||
handlerKind: 'function',
|
||||
file: 'src/handlers.ts',
|
||||
line: 8,
|
||||
handlerId: 'function:createUser',
|
||||
routeFile: 'src/routes.ts',
|
||||
routeLine: 4,
|
||||
routeId: 'route:src/routes.ts:4:POST:/users',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const palette = buildSearchPalette(
|
||||
[answer([result({ id: 'class:Users', name: 'Users', kind: 'class' })])],
|
||||
null,
|
||||
{ entries, query: 'users', entryRows: 6 }
|
||||
);
|
||||
|
||||
// Symbol matches keep the top: someone typing a name asked for the name.
|
||||
expect(palette.sections[0]?.title).toBe('Class');
|
||||
const last = palette.sections[palette.sections.length - 1];
|
||||
expect(last?.title).toBe('Entry points');
|
||||
const row = last?.items[0];
|
||||
expect(row?.type).toBe('entry');
|
||||
// The row a plain search cannot produce: the URL WITH its handler.
|
||||
expect(row?.name).toBe('POST /users');
|
||||
expect(row?.meta).toBe('createUser · handlers.ts:8');
|
||||
expect(row?.location).toBe('route');
|
||||
// The keyboard's flat list still equals what is drawn.
|
||||
expect(palette.items).toEqual(palette.sections.flatMap((s) => s.items));
|
||||
});
|
||||
|
||||
it('does not repeat a symbol the search above already found', () => {
|
||||
const hub = { ...result({ id: 'method:get', name: 'get' }), dependents: 264 };
|
||||
const entries = entryPoints({
|
||||
hubs: { total: 1, shown: 1, truncated: false, items: [hub] as any },
|
||||
});
|
||||
const palette = buildSearchPalette([answer([result({ id: 'method:get', name: 'get' })])], null, {
|
||||
entries,
|
||||
query: 'get',
|
||||
entryRows: 6,
|
||||
});
|
||||
expect(palette.sections.map((s) => s.title)).not.toContain('Entry points');
|
||||
});
|
||||
|
||||
it('draws nothing at all before the answer arrives', () => {
|
||||
const palette = buildEntryPalette(null);
|
||||
expect(palette.sections).toEqual([]);
|
||||
|
||||
Reference in New Issue
Block a user