// 落地页「确定层」推导:只用 report JSON 里已确定的结构, // 不依赖任何 AI / 人工,产出一张“项目地图”。 import type { Report, Func, Flow, Edge, ExternalSystem, Interaction } from "./types"; export interface OverviewProjectInfo { name: string; languages: string[]; fileCount: number; functionCount: number; edgeCount: number; loc?: number; } export interface HttpEndpoint { method: string; path: string; funcId: string; funcName: string; file?: string; flowId?: string; flowSize?: number; // 该入口调用链覆盖的函数数 touchesDb: boolean; // 调用链是否触达数据库 callsService: boolean; // 调用链是否发起外部服务请求 } export interface EndpointGroup { key: string; // 业务域,如 auth / tasks endpoints: HttpEndpoint[]; } export interface OtherEntry { funcId: string; funcName: string; file?: string; flowId?: string; flowSize?: number; touchesDb: boolean; callsService: boolean; } export interface ExternalSummary { databases: { name: string; engine?: string; tableCount: number; tables: string[] }[]; services: { name: string; protocol?: string; endpointCount: number; endpoints: string[] }[]; } export interface FuncRank { funcId: string; funcName: string; file?: string; fanIn: number; // 被多少处调用 } export interface FlowRank { flowId: string; name: string; size: number; // 覆盖函数数 touchesDb: boolean; callsService: boolean; } export interface Overview { project: OverviewProjectInfo; entrypoints: { total: number; httpGroups: EndpointGroup[]; httpCount: number; others: OtherEntry[]; }; externals: ExternalSummary; hotspots: { mostCalled: FuncRank[]; largestFlows: FlowRank[]; }; } // ---------- 工具 ---------- function apiInOf(fn: Func): Interaction | undefined { return (fn.interactions ?? []).find((it) => it.kind === "api" && it.direction === "in"); } function groupKeyFromPath(path: string): string { // /api/auth/sso-login -> auth ; /api/tasks/:id -> tasks ; /health -> health const segs = path.split("/").filter(Boolean); if (segs.length === 0) return "(root)"; const idx = segs[0] === "api" ? 1 : 0; return segs[idx] ?? segs[0]; } // 从某入口函数出发,沿调用/触发边可达的所有函数 id(有界 BFS) function reachableFuncs(startId: string, adjacency: Map, limit = 2000): Set { const seen = new Set([startId]); const queue = [startId]; while (queue.length && seen.size < limit) { const cur = queue.shift()!; for (const next of adjacency.get(cur) ?? []) { if (!seen.has(next)) { seen.add(next); queue.push(next); } } } return seen; } // ---------- 主推导 ---------- export function deriveOverview(report: Report): Overview { const funcById = new Map(); for (const fn of report.functions) funcById.set(fn.id, fn); // 调用/触发邻接表(用于可达性 & fan-in) const adjacency = new Map(); const fanIn = new Map(); for (const e of report.edges as Edge[]) { if (e.kind === "call" || e.kind === "trigger") { if (!adjacency.has(e.sourceId)) adjacency.set(e.sourceId, []); adjacency.get(e.sourceId)!.push(e.targetId); fanIn.set(e.targetId, (fanIn.get(e.targetId) ?? 0) + 1); } } // 入口函数 -> 对应的 call flow(用 entryNodeId 关联) const flowByEntry = new Map(); for (const fl of report.flows) { if (fl.entryNodeId) flowByEntry.set(fl.entryNodeId, fl); } // 判定一个函数集合是否触达 db / 外部服务 const dbFuncIds = new Set(); const svcFuncIds = new Set(); for (const fn of report.functions) { for (const it of fn.interactions ?? []) { if (it.kind === "db") dbFuncIds.add(fn.id); if (it.kind === "api" && it.direction === "out") svcFuncIds.add(fn.id); } } const analyzeEntry = (fn: Func) => { const flow = flowByEntry.get(fn.id); const reach = reachableFuncs(fn.id, adjacency); let touchesDb = false; let callsService = false; for (const id of reach) { if (dbFuncIds.has(id)) touchesDb = true; if (svcFuncIds.has(id)) callsService = true; } return { flowId: flow?.id, flowSize: flow ? flow.steps.length : reach.size, touchesDb, callsService, }; }; // 分类入口 const httpEndpoints: HttpEndpoint[] = []; const others: OtherEntry[] = []; for (const fn of report.functions) { if (!fn.isEntry) continue; const api = apiInOf(fn); const info = analyzeEntry(fn); if (api && api.kind === "api") { httpEndpoints.push({ method: api.method ?? "", path: api.path ?? "", funcId: fn.id, funcName: fn.name, file: fn.location?.file, ...info, }); } else { others.push({ funcId: fn.id, funcName: fn.name, file: fn.location?.file, ...info, }); } } // HTTP 分组 const groupMap = new Map(); for (const ep of httpEndpoints) { const key = groupKeyFromPath(ep.path); if (!groupMap.has(key)) groupMap.set(key, []); groupMap.get(key)!.push(ep); } const httpGroups: EndpointGroup[] = [...groupMap.entries()] .map(([key, endpoints]) => ({ key, endpoints: endpoints.sort((a, b) => a.path.localeCompare(b.path) || a.method.localeCompare(b.method)), })) .sort((a, b) => b.endpoints.length - a.endpoints.length || a.key.localeCompare(b.key)); // 外部系统汇总 const externals: ExternalSummary = { databases: [], services: [] }; for (const sys of (report.externalSystems ?? []) as ExternalSystem[]) { if (sys.kind === "database") { externals.databases.push({ name: sys.name, engine: sys.engine, tableCount: sys.tables.length, tables: sys.tables, }); } else { externals.services.push({ name: sys.name, protocol: sys.protocol, endpointCount: sys.endpoints.length, endpoints: sys.endpoints, }); } } // 热点:被调用最多的函数 const mostCalled: FuncRank[] = [...fanIn.entries()] .map(([funcId, count]) => { const fn = funcById.get(funcId); return { funcId, funcName: fn?.name ?? funcId, file: fn?.location?.file, fanIn: count }; }) .filter((r) => !!funcById.get(r.funcId)) .sort((a, b) => b.fanIn - a.fanIn) .slice(0, 10); // 热点:最大的调用链 const largestFlows: FlowRank[] = report.flows .map((fl) => { const entry = fl.entryNodeId ? funcById.get(fl.entryNodeId) : undefined; const info = entry ? analyzeEntry(entry) : { touchesDb: false, callsService: false }; return { flowId: fl.id, name: fl.name, size: fl.steps.length, touchesDb: info.touchesDb, callsService: info.callsService, }; }) .sort((a, b) => b.size - a.size) .slice(0, 10); return { project: { name: report.project.name, languages: report.project.languages, fileCount: report.stats.fileCount, functionCount: report.stats.functionCount, edgeCount: report.stats.edgeCount, loc: report.stats.loc, }, entrypoints: { total: httpEndpoints.length + others.length, httpGroups, httpCount: httpEndpoints.length, others: others.sort((a, b) => (b.flowSize ?? 0) - (a.flowSize ?? 0)), }, externals, hotspots: { mostCalled, largestFlows }, }; }