feat(expo-router): add Expo Router support for screens and navigates
Introduce Expo Router integration: a new framework resolver, route-based screen nodes, and navigates edges, plus a /api/screens endpoint and a Screens UI view. Adds branch-guard-driven labeling of edges, resolution logic, and tests to cover extraction, resolution, and end-to-end flow. This enables CodeGraph UI to surface screens and transitions from Expo Router apps.
This commit is contained in:
@@ -35,6 +35,7 @@ import type {
|
||||
WireFileCodePayload,
|
||||
WireFlowPayload,
|
||||
WireMapPayload,
|
||||
WireScreensPayload,
|
||||
WireNodeRefs,
|
||||
WireRoutes,
|
||||
WireSearch,
|
||||
@@ -210,6 +211,8 @@ export interface GraphAdapter {
|
||||
flow(request: FlowRequest, signal?: AbortSignal): Promise<WireFlowPayload>;
|
||||
/** The repository at module granularity, layered. */
|
||||
map(request?: MapRequest, signal?: AbortSignal): Promise<WireMapPayload>;
|
||||
/** The app's screens and the transitions between them, with their conditions. */
|
||||
screens(signal?: AbortSignal): Promise<WireScreensPayload>;
|
||||
/** The URL → handler map. */
|
||||
routes(request?: RoutesRequest, signal?: AbortSignal): Promise<WireRoutes>;
|
||||
/** Where a reader starts: routes, files that run something, tests, hubs. */
|
||||
@@ -393,6 +396,10 @@ export function createHttpAdapter(options: HttpAdapterOptions = {}): GraphAdapte
|
||||
return getJson<WireRoutes>(`api/routes${query(params)}`, signal);
|
||||
},
|
||||
|
||||
screens(signal) {
|
||||
return getJson<WireScreensPayload>('api/screens', signal);
|
||||
},
|
||||
|
||||
entryPoints(request = {}, signal) {
|
||||
const params = new URLSearchParams();
|
||||
if (request.limit) params.set('limit', String(request.limit));
|
||||
|
||||
@@ -19,6 +19,7 @@ import type {
|
||||
WireFileCodePayload,
|
||||
WireFlowPayload,
|
||||
WireMapPayload,
|
||||
WireScreensPayload,
|
||||
WireNodeRefs,
|
||||
WireRoutes,
|
||||
WireSearch,
|
||||
@@ -141,6 +142,10 @@ export function fetchSource(
|
||||
* is how many path segments under it name a module. Omitting `root` lets the
|
||||
* adapter pick the repository's source directory.
|
||||
*/
|
||||
export function fetchScreens(signal?: AbortSignal): Promise<WireScreensPayload> {
|
||||
return getGraphAdapter().screens(signal);
|
||||
}
|
||||
|
||||
export function fetchMap(
|
||||
opts: { root?: string | null; depth?: number } = {},
|
||||
signal?: AbortSignal
|
||||
|
||||
@@ -243,7 +243,9 @@ export function buildEntryPanel(entries: WireEntryPoints | null): EntryPanel {
|
||||
section(
|
||||
'routes',
|
||||
'Routes',
|
||||
'A request from outside arrives here — the URL, and the symbol that serves it.',
|
||||
entries.routes.items.items.every((r) => !r.method)
|
||||
? 'A screen of the app — its path, and the component that renders it.'
|
||||
: 'A request from outside arrives here — the URL, and the symbol that serves it.',
|
||||
entries.routes.items,
|
||||
groupRows(
|
||||
entries.routes.items.items.map((route) => ({
|
||||
|
||||
+28
-3
@@ -186,6 +186,22 @@ export interface MapLayout {
|
||||
|
||||
export interface MapLayoutOptions {
|
||||
includeTests: boolean;
|
||||
/** Override the hidden-link floor; 0 draws every link (the Screens view). */
|
||||
minWeight?: number;
|
||||
/**
|
||||
* The two lines a box is sized for. The Map's boxes show the module id and
|
||||
* its counts; a view that shows something else (a screen's path and its
|
||||
* component) must size for what it draws, or an opaque id decides the width.
|
||||
*/
|
||||
sizing?: (module: WireMapModule, island: boolean) => { label: string; meta: string };
|
||||
/**
|
||||
* Replace longest-path layering. Receives every module id and the acyclic
|
||||
* links (mutual pairs already broken); returns each id's layer, 0 at the
|
||||
* BOTTOM. The Screens view lays out by distance from the entry screen,
|
||||
* where "one layer above what it depends on" would put the head of the
|
||||
* longest chain of screens above the login page.
|
||||
*/
|
||||
layering?: (ids: string[], links: ReadonlyArray<{ source: string; target: string }>) => Map<string, number>;
|
||||
}
|
||||
|
||||
export function strokeWidthFor(count: number): number {
|
||||
@@ -212,7 +228,7 @@ export function buildMapLayout(
|
||||
// depends on is depended on, whatever this screen is currently showing.
|
||||
const depended = new Set(payload.links.map((l) => l.target));
|
||||
const links = payload.links.filter((l) => present.has(l.source) && present.has(l.target));
|
||||
const minWeight = options.includeTests ? MIN_WEIGHT_WITH_TESTS : MIN_WEIGHT;
|
||||
const minWeight = options.minWeight ?? (options.includeTests ? MIN_WEIGHT_WITH_TESTS : MIN_WEIGHT);
|
||||
|
||||
const declaredLinks = links.filter((l) => l.declared > 0);
|
||||
const useDeclared =
|
||||
@@ -246,7 +262,12 @@ export function buildMapLayout(
|
||||
for (const list of out.values()) list.sort();
|
||||
|
||||
const layer = new Map<string, number>();
|
||||
for (const module of modules) longestPath(module.id, out, layer, new Set());
|
||||
if (options.layering) {
|
||||
for (const [id, value] of options.layering(modules.map((m) => m.id), acyclic)) layer.set(id, value);
|
||||
for (const module of modules) if (!layer.has(module.id)) layer.set(module.id, 0);
|
||||
} else {
|
||||
for (const module of modules) longestPath(module.id, out, layer, new Set());
|
||||
}
|
||||
|
||||
const layerCount = Math.max(1, ...[...layer.values()].map((v) => v + 1));
|
||||
const rows: string[][] = Array.from({ length: layerCount }, () => []);
|
||||
@@ -282,7 +303,11 @@ export function buildMapLayout(
|
||||
// --- placement -----------------------------------------------------------
|
||||
const islands = new Set(modules.filter((m) => !depended.has(m.id)).map((m) => m.id));
|
||||
const widths = new Map(
|
||||
modules.map((m) => [m.id, nodeWidth(m.id, moduleMetaLabel(m, islands.has(m.id)))])
|
||||
modules.map((m) => {
|
||||
const island = islands.has(m.id);
|
||||
const lines = options.sizing?.(m, island) ?? { label: m.id, meta: moduleMetaLabel(m, island) };
|
||||
return [m.id, nodeWidth(lines.label, lines.meta)];
|
||||
})
|
||||
);
|
||||
const rowSums = rows.map((row) => row.reduce((sum, id) => sum + (widths.get(id) ?? 0), 0));
|
||||
// Natural span = the boxes shoulder to shoulder. The content width is the
|
||||
|
||||
@@ -68,6 +68,7 @@ export interface NavigationDriver {
|
||||
mapHref(opts?: MapHrefOptions): string;
|
||||
flowHref(opts?: FlowHrefOptions): string;
|
||||
entryHref(): string;
|
||||
screensHref(): string;
|
||||
deadHref(opts?: DeadCodeHrefOptions): string;
|
||||
/** Go to an href this driver built. */
|
||||
navigate(href: string, opts?: { replace?: boolean }): void;
|
||||
@@ -133,6 +134,10 @@ export const hashNavigation: NavigationDriver = {
|
||||
return '#/entry';
|
||||
},
|
||||
|
||||
screensHref() {
|
||||
return '#/screens';
|
||||
},
|
||||
|
||||
deadHref(opts = {}) {
|
||||
const params = new URLSearchParams();
|
||||
if (opts.exported) params.set('exported', '1');
|
||||
@@ -212,6 +217,10 @@ export function entryHref(): string {
|
||||
return driver.entryHref();
|
||||
}
|
||||
|
||||
export function screensHref(): string {
|
||||
return driver.screensHref();
|
||||
}
|
||||
|
||||
export function deadHref(opts: DeadCodeHrefOptions = {}): string {
|
||||
return driver.deadHref(opts);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
* #/map module map (?root=&depth=&tests=1)
|
||||
* #/flow flow strip (?from=&to= | ?symbols= | ?t=<trail>)
|
||||
* #/entry entry points (where a flow starts)
|
||||
* #/screens screens (the app's screens and transitions)
|
||||
* #/dead dead code (?exported=1 widens the claim)
|
||||
*
|
||||
* Node ids are opaque engine strings shaped `<kind>:<hash>` or
|
||||
@@ -40,6 +41,7 @@ export {
|
||||
hashNavigation,
|
||||
mapHref,
|
||||
navigate,
|
||||
screensHref,
|
||||
setNavigationDriver,
|
||||
symbolHref,
|
||||
} from './navigation';
|
||||
@@ -74,6 +76,7 @@ export type Route =
|
||||
trail: string | null;
|
||||
}
|
||||
| { view: 'entry' }
|
||||
| { view: 'screens' }
|
||||
| {
|
||||
view: 'dead';
|
||||
/** Symbols reachable from outside the index are on the list. */
|
||||
@@ -136,6 +139,8 @@ export function parseHash(hash: string): RouterLocation {
|
||||
};
|
||||
} else if (head === 'entry' && rest.length === 0) {
|
||||
route = { view: 'entry' };
|
||||
} else if (head === 'screens' && rest.length === 0) {
|
||||
route = { view: 'screens' };
|
||||
} else if (head === 'dead' && rest.length === 0) {
|
||||
// The widening travels in the URL like the map's shape does: a link to
|
||||
// "including exported symbols" has to reopen the same list.
|
||||
|
||||
@@ -0,0 +1,277 @@
|
||||
/**
|
||||
* The Screens view's model — the app's screens and the transitions between
|
||||
* them, laid out so that a screen sits above the screens it opens.
|
||||
*
|
||||
* The layout is the Map's (`buildMapLayout`): the same longest-path layering,
|
||||
* the same barycenter ordering, the same ports, the same determinism. A
|
||||
* screen graph is a module graph with different words — nodes with names,
|
||||
* weighted links that mostly point one way, a few cycles (Home ↔ Capture)
|
||||
* that become dashed back-edges rather than being straightened into a lie.
|
||||
* Reusing it means a reader who learned the Map reads this without learning
|
||||
* anything new, and means this file is mostly translation, not geometry.
|
||||
*
|
||||
* What is this file's own: which links share a pair (several transitions from
|
||||
* Home to Capture, each with its own condition, draw as ONE edge whose label
|
||||
* counts them), the words on that edge, and the two lists the side panel
|
||||
* shows for a selected screen.
|
||||
*/
|
||||
|
||||
import type { WireMapLink, WireMapModule, WireScreen, WireScreenLink, WireScreensPayload } from './wire';
|
||||
import { buildMapLayout, linkId, type MapLayout } from './map-model';
|
||||
|
||||
/** The longest `when` a connector prints before an ellipsis; the tooltip has the rest. */
|
||||
const EDGE_LABEL_MAX = 30;
|
||||
|
||||
export interface ScreenNodeInfo {
|
||||
id: string;
|
||||
/** `/object-detail`, or a function name for an origin. */
|
||||
label: string;
|
||||
/** The component's name for a screen; the file for an origin. */
|
||||
sub: string;
|
||||
screen: WireScreen | null;
|
||||
/** A navigation that could not be attributed to a screen. */
|
||||
origin: boolean;
|
||||
entry: boolean;
|
||||
/** No path of transitions leads here from the entry screen. */
|
||||
unreached: boolean;
|
||||
}
|
||||
|
||||
export interface ScreenEdgeInfo {
|
||||
id: string;
|
||||
from: string;
|
||||
to: string;
|
||||
/** Every transition between the pair — one connector, several stories. */
|
||||
links: WireScreenLink[];
|
||||
/** The connector's short label: the condition, or how many transitions. */
|
||||
label: string;
|
||||
synthesized: boolean;
|
||||
}
|
||||
|
||||
export interface ScreensModel {
|
||||
layout: MapLayout;
|
||||
nodes: Map<string, ScreenNodeInfo>;
|
||||
/** Keyed by the layout edge's id (see `linkId`). */
|
||||
edges: Map<string, ScreenEdgeInfo>;
|
||||
/** Screens no chain of transitions reaches from the entry. */
|
||||
unreached: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Layer = distance from the entry screen: the entry on top, each row down one
|
||||
* more transition away. Origins (chrome, triggers outside any screen) count
|
||||
* as reachable seeds too, so what they open is placed below them. Whatever
|
||||
* nothing reaches sits in a band at the bottom, layered among itself by the
|
||||
* same rule from its own sources — a screen the graph cannot see anyone open
|
||||
* is a fact worth a place, not a crash.
|
||||
*/
|
||||
export function entryLayering(entry: string | null, seeds: readonly string[]) {
|
||||
return (ids: string[], links: ReadonlyArray<{ source: string; target: string }>): Map<string, number> => {
|
||||
const out = new Map<string, string[]>(ids.map((id) => [id, []]));
|
||||
const indeg = new Map<string, number>(ids.map((id) => [id, 0]));
|
||||
for (const l of links) {
|
||||
out.get(l.source)?.push(l.target);
|
||||
indeg.set(l.target, (indeg.get(l.target) ?? 0) + 1);
|
||||
}
|
||||
const depth = new Map<string, number>();
|
||||
const bfs = (starts: string[]) => {
|
||||
let frontier = starts.filter((s) => !depth.has(s));
|
||||
for (const s of frontier) depth.set(s, 0);
|
||||
let d = 0;
|
||||
while (frontier.length > 0) {
|
||||
d++;
|
||||
const next: string[] = [];
|
||||
for (const id of frontier) {
|
||||
for (const t of out.get(id) ?? []) {
|
||||
if (depth.has(t)) continue;
|
||||
depth.set(t, d);
|
||||
next.push(t);
|
||||
}
|
||||
}
|
||||
frontier = next;
|
||||
}
|
||||
};
|
||||
const roots = [entry, ...seeds].filter((s): s is string => s !== null && ids.includes(s));
|
||||
bfs(roots);
|
||||
const reachedMax = Math.max(0, ...[...depth.values()]);
|
||||
// The unreached band: its own sources first, then whatever they open.
|
||||
const rest = ids.filter((id) => !depth.has(id));
|
||||
const restDepth = new Map<string, number>();
|
||||
if (rest.length > 0) {
|
||||
const restSources = rest.filter((id) => (indeg.get(id) ?? 0) === 0);
|
||||
const seedsRest = restSources.length > 0 ? restSources : [rest[0]!];
|
||||
let frontier = seedsRest;
|
||||
for (const s of frontier) restDepth.set(s, 0);
|
||||
let d = 0;
|
||||
while (frontier.length > 0) {
|
||||
d++;
|
||||
const next: string[] = [];
|
||||
for (const id of frontier) {
|
||||
for (const t of out.get(id) ?? []) {
|
||||
if (restDepth.has(t) || depth.has(t)) continue;
|
||||
restDepth.set(t, d);
|
||||
next.push(t);
|
||||
}
|
||||
}
|
||||
frontier = next;
|
||||
}
|
||||
for (const id of rest) if (!restDepth.has(id)) restDepth.set(id, 0);
|
||||
}
|
||||
const restMax = Math.max(0, ...[...restDepth.values()]);
|
||||
// Layer 0 is the bottom. Unreached band occupies [0, restMax]; reached
|
||||
// screens sit above it, the entry highest, with one empty row between.
|
||||
const base = rest.length > 0 ? restMax + 2 : 0;
|
||||
const layer = new Map<string, number>();
|
||||
for (const [id, d] of depth) layer.set(id, base + reachedMax - d);
|
||||
for (const [id, d] of restDepth) layer.set(id, restMax - d);
|
||||
return layer;
|
||||
};
|
||||
}
|
||||
|
||||
/** What the connector says. Empty when unconditional and single. */
|
||||
export function edgeLabel(links: readonly WireScreenLink[]): string {
|
||||
if (links.length === 1) {
|
||||
const when = links[0]!.when;
|
||||
if (!when) return '';
|
||||
return when.length > EDGE_LABEL_MAX ? `${when.slice(0, EDGE_LABEL_MAX - 1)}…` : when;
|
||||
}
|
||||
const conditional = links.filter((l) => l.when).length;
|
||||
return conditional > 0 ? `${links.length} ways · ${conditional} conditional` : `${links.length} ways`;
|
||||
}
|
||||
|
||||
export function buildScreensModel(payload: WireScreensPayload): ScreensModel {
|
||||
const nodes = new Map<string, ScreenNodeInfo>();
|
||||
const modules: WireMapModule[] = [];
|
||||
const used = new Set<string>();
|
||||
for (const link of payload.links) {
|
||||
used.add(link.from);
|
||||
used.add(link.to);
|
||||
}
|
||||
|
||||
for (const screen of payload.screens) {
|
||||
const info: ScreenNodeInfo = {
|
||||
id: screen.id,
|
||||
label: screen.path,
|
||||
sub: screen.component?.name ?? screen.file,
|
||||
screen,
|
||||
origin: false,
|
||||
entry: payload.entry === screen.id,
|
||||
unreached: false,
|
||||
};
|
||||
nodes.set(screen.id, info);
|
||||
modules.push(moduleFor(info, screen.incoming + screen.outgoing));
|
||||
}
|
||||
for (const origin of payload.origins) {
|
||||
const info: ScreenNodeInfo = {
|
||||
id: origin.id,
|
||||
label: origin.node.kind === 'component' ? `<${origin.node.name}>` : `${origin.node.name}()`,
|
||||
sub: origin.sharedBy ? `on ${origin.sharedBy} screens` : origin.node.file,
|
||||
screen: null,
|
||||
origin: true,
|
||||
entry: false,
|
||||
unreached: false,
|
||||
};
|
||||
nodes.set(origin.id, info);
|
||||
modules.push(moduleFor(info, origin.outgoing));
|
||||
}
|
||||
|
||||
// One layout link per (from, to); the transitions behind it stay listed.
|
||||
const byPair = new Map<string, WireScreenLink[]>();
|
||||
for (const link of payload.links) {
|
||||
const key = linkId({ source: link.from, target: link.to });
|
||||
const list = byPair.get(key) ?? [];
|
||||
list.push(link);
|
||||
byPair.set(key, list);
|
||||
}
|
||||
const links: WireMapLink[] = [];
|
||||
const edges = new Map<string, ScreenEdgeInfo>();
|
||||
for (const [key, group] of byPair) {
|
||||
const first = group[0]!;
|
||||
if (!nodes.has(first.from) || !nodes.has(first.to)) continue;
|
||||
// A screen that reopens itself (a retry) is a fact for the panel, not an
|
||||
// arrow the layout can draw.
|
||||
if (first.from === first.to) continue;
|
||||
links.push({
|
||||
source: first.from,
|
||||
target: first.to,
|
||||
count: group.length,
|
||||
declared: group.length,
|
||||
byKind: [{ kind: 'navigates', count: group.length }],
|
||||
topPairs: [],
|
||||
});
|
||||
edges.set(key, {
|
||||
id: key,
|
||||
from: first.from,
|
||||
to: first.to,
|
||||
links: group,
|
||||
label: edgeLabel(group),
|
||||
synthesized: group.every((l) => l.synthesized),
|
||||
});
|
||||
}
|
||||
|
||||
// Reachability from the entry (and from the origins, which are entries of
|
||||
// a kind: chrome is on the screen the user is on).
|
||||
const seeds = payload.origins.map((o) => o.id);
|
||||
const reachable = new Set<string>();
|
||||
{
|
||||
const out = new Map<string, string[]>();
|
||||
for (const l of payload.links) out.set(l.from, [...(out.get(l.from) ?? []), l.to]);
|
||||
const stack = [payload.entry, ...seeds].filter((s): s is string => s !== null);
|
||||
while (stack.length > 0) {
|
||||
const id = stack.pop()!;
|
||||
if (reachable.has(id)) continue;
|
||||
reachable.add(id);
|
||||
for (const t of out.get(id) ?? []) stack.push(t);
|
||||
}
|
||||
}
|
||||
let unreached = 0;
|
||||
for (const info of nodes.values()) {
|
||||
if (!info.origin && !reachable.has(info.id)) {
|
||||
info.unreached = true;
|
||||
unreached++;
|
||||
}
|
||||
}
|
||||
|
||||
const layout = buildMapLayout(
|
||||
{ modules, links },
|
||||
{
|
||||
includeTests: true,
|
||||
minWeight: 0,
|
||||
sizing: (m) => {
|
||||
const info = nodes.get(m.id);
|
||||
return { label: info?.label ?? m.id, meta: info?.sub ?? '' };
|
||||
},
|
||||
layering: entryLayering(payload.entry, seeds),
|
||||
}
|
||||
);
|
||||
return { layout, nodes, edges, unreached };
|
||||
}
|
||||
|
||||
function moduleFor(info: ScreenNodeInfo, symbols: number): WireMapModule {
|
||||
return {
|
||||
id: info.id,
|
||||
label: info.label,
|
||||
files: 1,
|
||||
symbols,
|
||||
languages: [],
|
||||
test: false,
|
||||
generated: 0,
|
||||
generatedFiles: [],
|
||||
facade: false,
|
||||
fileList: { total: 1, shown: 1, truncated: false, items: [info.screen?.file ?? info.sub] },
|
||||
};
|
||||
}
|
||||
|
||||
/** The side panel's two lists for a selected node. */
|
||||
export function neighbourhood(
|
||||
payload: WireScreensPayload,
|
||||
id: string
|
||||
): { opensFrom: WireScreenLink[]; goesTo: WireScreenLink[] } {
|
||||
const opensFrom = payload.links.filter((l) => l.to === id);
|
||||
const goesTo = payload.links.filter((l) => l.from === id);
|
||||
return { opensFrom, goesTo };
|
||||
}
|
||||
|
||||
/** `ItemCard → openObjectDetail`, or '' when the screen's own component navigates. */
|
||||
export function viaText(link: WireScreenLink): string {
|
||||
return link.via.map((v) => v.name).join(' → ');
|
||||
}
|
||||
@@ -269,7 +269,9 @@ export function buildEntryPalette(
|
||||
if (entries.routes.routed && entries.routes.items.items.length > 0) {
|
||||
sections.push({
|
||||
title: 'Routes',
|
||||
note: 'A request from outside arrives here.',
|
||||
note: entries.routes.items.items.every((r) => !r.method)
|
||||
? 'A screen of the app.'
|
||||
: 'A request from outside arrives here.',
|
||||
items: take(entries.routes.items.items).map((route) => ({
|
||||
type: 'route' as const,
|
||||
id: `route:${route.routeId}`,
|
||||
|
||||
@@ -65,6 +65,8 @@ export function edgeWord(edge: WireEdge): string {
|
||||
return '';
|
||||
case 'instantiates':
|
||||
return 'creates';
|
||||
case 'navigates':
|
||||
return 'navigates to';
|
||||
case 'references':
|
||||
return edge.valueRef ? 'passes as value' : 'uses type';
|
||||
default:
|
||||
@@ -82,6 +84,16 @@ export function relationWords(relation: WireRelation): string[] {
|
||||
return words;
|
||||
}
|
||||
|
||||
/** The distinct branch conditions across a relation's edges, at most three. */
|
||||
export function relationWhens(relation: WireRelation): string[] {
|
||||
const out: string[] = [];
|
||||
for (const edge of relation.edges) {
|
||||
if (edge.when && !out.includes(edge.when)) out.push(edge.when);
|
||||
if (out.length === 3) break;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** The synthesizer that produced this relation's edge, when one did. */
|
||||
export function synthesizedBy(relation: WireRelation): string | null {
|
||||
if (!relation.synthesized) return null;
|
||||
@@ -335,6 +347,8 @@ export interface CalleeRow {
|
||||
lines: number[];
|
||||
words: string[];
|
||||
via: string | null;
|
||||
/** `when` conditions, distinct, for the meta line. */
|
||||
when: string[];
|
||||
}
|
||||
|
||||
export interface CalleeRailModel {
|
||||
@@ -357,6 +371,7 @@ export function buildCalleeRail(payload: WireSymbolPayload): CalleeRailModel {
|
||||
lines: relation.lines,
|
||||
words: relationWords(relation),
|
||||
via: synthesizedBy(relation),
|
||||
when: relationWhens(relation),
|
||||
};
|
||||
if (relation.uncertain) uncertain.push(row);
|
||||
else rows.push(row);
|
||||
@@ -380,6 +395,8 @@ export interface CallerRow {
|
||||
/** Call-site lines in the CALLER's file — the `:4657` chips. */
|
||||
lines: number[];
|
||||
via: string | null;
|
||||
/** `when` conditions, distinct, for the meta line. */
|
||||
when: string[];
|
||||
}
|
||||
|
||||
export interface CallerFileGroup {
|
||||
@@ -420,6 +437,7 @@ export function buildCallerRail(payload: WireSymbolPayload): CallerRailModel {
|
||||
words: relationWords(relation),
|
||||
lines: relation.lines,
|
||||
via: synthesizedBy(relation),
|
||||
when: relationWhens(relation),
|
||||
};
|
||||
// Uncertainty wins over test-ness: a name-only guess is a claim about the
|
||||
// edge, and burying it in the tests fold would present it as established.
|
||||
|
||||
@@ -117,6 +117,8 @@ export interface WireEdge {
|
||||
via?: string;
|
||||
registeredAt?: string;
|
||||
valueRef?: boolean;
|
||||
/** Branch conditions the call site runs under — `!isUploading && isCollected`. */
|
||||
when?: string;
|
||||
}
|
||||
|
||||
/** Every edge between the focal symbol and ONE other symbol, as a single row. */
|
||||
@@ -642,6 +644,56 @@ export interface WireMapPayload {
|
||||
timing: { elapsedMs: number; cached: boolean };
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- screens -- */
|
||||
|
||||
export interface WireScreen {
|
||||
id: string;
|
||||
path: string;
|
||||
file: string;
|
||||
line: number;
|
||||
component: WireNodeRef | null;
|
||||
incoming: number;
|
||||
outgoing: number;
|
||||
}
|
||||
|
||||
export interface WireScreenOrigin {
|
||||
id: string;
|
||||
node: WireNodeRef;
|
||||
outgoing: number;
|
||||
/** Shared chrome: how many screens render it. */
|
||||
sharedBy?: number;
|
||||
}
|
||||
|
||||
export interface WireScreenSite {
|
||||
file: string;
|
||||
line: number;
|
||||
href: string;
|
||||
method: string;
|
||||
when: string;
|
||||
}
|
||||
|
||||
export interface WireScreenLink {
|
||||
id: string;
|
||||
from: string;
|
||||
to: string;
|
||||
fromOrigin: boolean;
|
||||
via: WireNodeRef[];
|
||||
when: string;
|
||||
sites: WireScreenSite[];
|
||||
synthesized: boolean;
|
||||
}
|
||||
|
||||
export interface WireScreensPayload {
|
||||
routed: boolean;
|
||||
entry: string | null;
|
||||
screens: WireScreen[];
|
||||
origins: WireScreenOrigin[];
|
||||
links: WireScreenLink[];
|
||||
dropped: number;
|
||||
index: { lastIndexedAt: number | null; edges: number; files: number };
|
||||
timing: { elapsedMs: number };
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------- dead code -- */
|
||||
|
||||
/** One symbol nothing in the index reaches. */
|
||||
|
||||
Reference in New Issue
Block a user