wire.ts 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589
  1. /**
  2. * The wire shapes of the graph API — types only, no runtime.
  3. *
  4. * These mirror the server's payloads (`src/ui-server/api/`, CG-42) rather than
  5. * re-deriving them: the API is versioned with the binary that serves it, so a
  6. * field the server stopped sending should break the type-check here, not
  7. * surface as `undefined` in a rail three screens later.
  8. *
  9. * They are also the vocabulary of {@link GraphAdapter} (`adapter.ts`): a host
  10. * embedding these components answers in exactly these shapes, whether it is
  11. * reading them over HTTP from `codegraph ui` or building them in-process from
  12. * its own engine. Keeping them in a file with no imports and no side effects is
  13. * what lets a host depend on the vocabulary without pulling in the transport.
  14. */
  15. import type { WireHighlight } from './highlight';
  16. /* ---------------------------------------------------------------- shapes -- */
  17. export type NodeKind = string;
  18. export type EdgeKind = string;
  19. export interface WireNodeRef {
  20. id: string;
  21. kind: NodeKind;
  22. name: string;
  23. qualifiedName: string;
  24. /** Project-relative, forward slashes on every platform. */
  25. file: string;
  26. line: number;
  27. endLine: number;
  28. language: string;
  29. signature?: string;
  30. exported?: boolean;
  31. /** Lives in a file that looks like test or fixture code. */
  32. test: boolean;
  33. }
  34. export interface WireNodeDetail extends WireNodeRef {
  35. startColumn: number;
  36. endColumn: number;
  37. docstring?: string;
  38. visibility?: string;
  39. async?: boolean;
  40. static?: boolean;
  41. abstract?: boolean;
  42. decorators?: string[];
  43. typeParameters?: string[];
  44. returnType?: string;
  45. lines: number;
  46. }
  47. export interface WireMember extends WireNodeRef {
  48. parentId: string;
  49. /** 1 = a direct member; 2 = a member of a member (a method inside a file's class). */
  50. depth: number;
  51. fanIn: number;
  52. fanOut: number;
  53. }
  54. export interface WireEdge {
  55. kind: EdgeKind;
  56. line?: number;
  57. col?: number;
  58. confidence?: number;
  59. resolvedBy?: string;
  60. provenance?: string;
  61. synthesizedBy?: string;
  62. via?: string;
  63. registeredAt?: string;
  64. valueRef?: boolean;
  65. }
  66. /** Every edge between the focal symbol and ONE other symbol, as a single row. */
  67. export interface WireRelation {
  68. node: WireNodeRef;
  69. edgeKinds: EdgeKind[];
  70. edges: WireEdge[];
  71. edgeCount: number;
  72. /** Distinct call-site lines, ascending — what the gutter ports anchor to. */
  73. lines: number[];
  74. confidence: number | null;
  75. uncertain: boolean;
  76. synthesized: boolean;
  77. fanIn?: number;
  78. hub?: boolean;
  79. }
  80. export interface WireList<T> {
  81. total: number;
  82. shown: number;
  83. truncated: boolean;
  84. items: T[];
  85. }
  86. export interface WireTestSummary {
  87. reached: boolean;
  88. hops: number | null;
  89. fileCount: number;
  90. files: string[];
  91. /** False weakens the claim to "no test calls this directly" — see the server. */
  92. exhaustive: boolean;
  93. hopsSearched: number;
  94. }
  95. export interface WireOutsideIndex {
  96. total: number;
  97. byKind: Record<string, number>;
  98. samples: Array<{ name: string; kind: string; line?: number; col?: number }>;
  99. }
  100. export interface WireBlastSummary {
  101. direct: number;
  102. withinHops: number;
  103. hops: number;
  104. files: number;
  105. testFiles: number;
  106. routes: number;
  107. topFiles: Array<{ file: string; symbols: number; test: boolean }>;
  108. }
  109. export interface WireSymbolPayload {
  110. node: WireNodeDetail;
  111. /** Outermost first: file, then module/class, then the symbol's own parent. */
  112. ancestors: WireNodeRef[];
  113. members: WireList<WireMember>;
  114. incoming: WireList<WireRelation>;
  115. outgoing: WireList<WireRelation>;
  116. typesUsed: WireRelation[];
  117. counts: {
  118. callers: number;
  119. callees: number;
  120. typesUsed: number;
  121. fanIn: number;
  122. fanOut: number;
  123. members: number;
  124. hub: boolean;
  125. };
  126. tests: WireTestSummary;
  127. outsideIndex: WireOutsideIndex;
  128. blast: WireBlastSummary | null;
  129. /** The file changed on disk since the index — line ranges may be shifted. */
  130. drift: boolean;
  131. }
  132. export interface WireSource {
  133. file: string;
  134. language: string;
  135. drift: boolean;
  136. /**
  137. * Which numbering `lines` belong to. `'indexed'` — the file matches the
  138. * index. `'current'` — it drifted and we asked for the bytes anyway
  139. * (`ondrift: 'current'`), so nothing the graph holds about this file lines up
  140. * with them. `'none'` — it drifted and no slice came back.
  141. */
  142. showing: 'indexed' | 'current' | 'none';
  143. contentHash: string;
  144. indexedAt: number;
  145. generated: boolean;
  146. totalLines: number | null;
  147. from?: number;
  148. to?: number;
  149. /** Absent when the file drifted and `ondrift` was left at its default. */
  150. lines?: string[];
  151. truncated?: boolean;
  152. reason?: string;
  153. /**
  154. * The same lines, classified by the server's tree-sitter parse — one entry
  155. * per line, each a list of `[classId, text]` pairs indexed into `classes`.
  156. * Absent whenever `lines` is, and `engine: 'plain'` whenever no grammar
  157. * covers the file. See `lib/highlight.ts`.
  158. */
  159. highlight?: WireHighlight;
  160. }
  161. /* ------------------------------------------------------------- file view -- */
  162. /** A row in the file outline — a symbol, its nesting and its edge counts. */
  163. export interface WireOutlineEntry extends WireNodeRef {
  164. /** Containing symbol within this file, or null for a top-level one. */
  165. parentId: string | null;
  166. /** Nesting depth from the top level of the file, starting at 0. */
  167. depth: number;
  168. fanIn: number;
  169. fanOut: number;
  170. }
  171. /** One file at the far end of an import rail, with the symbols the edges name. */
  172. export interface WireImportRow {
  173. file: string;
  174. test: boolean;
  175. symbols: Array<{ id: string; name: string; kind: string; line: number }>;
  176. symbolCount: number;
  177. }
  178. export interface WireFilePayload {
  179. file: {
  180. path: string;
  181. language: string;
  182. size: number;
  183. modifiedAt: number;
  184. indexedAt: number;
  185. contentHash: string;
  186. nodeCount: number;
  187. generated: boolean;
  188. test: boolean;
  189. errors: string[];
  190. /** The file node's own id, so the viewer can open the file AS a symbol. */
  191. id: string | null;
  192. };
  193. /** Calls made outside every definition — module-level code. */
  194. topLevel: { calls: number };
  195. /** The file changed on disk since it was indexed; the outline's lines shifted. */
  196. drift: boolean;
  197. outline: WireList<WireOutlineEntry>;
  198. /** `imports` edges only — a subset of `dependencies`, with symbol names. */
  199. imports: WireList<WireImportRow>;
  200. importedBy: WireList<WireImportRow>;
  201. /** Import statements that resolved to nothing indexed: packages, builtins. */
  202. unresolvedImports: Array<{ name: string; line: number }>;
  203. /** Every file this one reaches by any cross-file edge — `getFileDependencies`. */
  204. dependencies: string[];
  205. /** Every file that reaches into this one — `getFileDependents`. */
  206. dependents: string[];
  207. }
  208. /* ------------------------------------------------ whole-file source view -- */
  209. /** A reference the resolver never landed: a gutter port with no destination. */
  210. export interface WireFileOutsideRef {
  211. line: number;
  212. col: number;
  213. name: string;
  214. kind: string;
  215. }
  216. /** Every edge from ONE symbol in a file to ONE symbol anywhere. */
  217. export interface WireFileCall {
  218. /** The symbol making the calls — the file node itself for top-level code. */
  219. ownerId: string;
  220. ownerLine: number;
  221. relation: WireRelation;
  222. }
  223. export interface WireFileCodePayload {
  224. file: {
  225. path: string;
  226. language: string;
  227. size: number;
  228. indexedAt: number;
  229. contentHash: string;
  230. generated: boolean;
  231. test: boolean;
  232. errors: string[];
  233. id: string | null;
  234. /** Lines on disk now — the height of the scrolling document. */
  235. totalLines: number | null;
  236. };
  237. drift: boolean;
  238. reason?: string;
  239. outline: WireList<WireOutlineEntry>;
  240. calls: WireList<WireFileCall>;
  241. outside: WireList<WireFileOutsideRef>;
  242. /** Calls landing on a definition in this same file — the arc diagram's total. */
  243. intraFileCalls: number;
  244. timing: { elapsedMs: number };
  245. }
  246. export interface WireBlastScale {
  247. maxDirect: number;
  248. maxWithinHops: number;
  249. hops: number;
  250. sampled: number;
  251. estimated: boolean;
  252. }
  253. /* ------------------------------------------------------- search palette -- */
  254. /** How a result's text matched the query — the server's primary sort key. */
  255. export type MatchKind = 'exact' | 'prefix' | 'substring' | 'qualified' | 'file' | 'related';
  256. export interface WireSearchResult extends WireNodeRef {
  257. matchKind: MatchKind;
  258. }
  259. export interface WireSearchGroup {
  260. kind: NodeKind;
  261. count: number;
  262. items: WireSearchResult[];
  263. }
  264. export interface WireSearch {
  265. query: string;
  266. /** The free-text part, with any `kind:` / `lang:` / `path:` filters removed. */
  267. text: string;
  268. filters: { kinds: string[]; languages: string[]; paths: string[]; names: string[] };
  269. results: WireList<WireSearchResult>;
  270. /** Kind buckets in ranked order — flattening them reproduces the ranking. */
  271. groups: WireSearchGroup[];
  272. }
  273. export interface WireNodeRefs {
  274. items: WireNodeRef[];
  275. /** Ids that name nothing in this index — a stale link, not an error. */
  276. missing: string[];
  277. }
  278. /* --------------------------------------------------------------- routes -- */
  279. /** One row of the URL -> handler map (`/api/routes`). */
  280. export interface WireRoute {
  281. /** The route node's name, verbatim: "POST /v1/users/{id}". */
  282. url: string;
  283. /** The verb, when the name leads with one. Null for a file-routed page. */
  284. method: string | null;
  285. /** The URL without the verb — the same string as `url` when there is none. */
  286. path: string;
  287. handler: string;
  288. handlerKind: string;
  289. /** Where the request is SERVED. */
  290. file: string;
  291. line: number;
  292. handlerId: string | null;
  293. /** Where the URL is REGISTERED — the router file, which is how routes group. */
  294. routeFile: string;
  295. routeLine: number;
  296. routeId: string;
  297. }
  298. export interface WireRoutes {
  299. routed: boolean;
  300. /** Every URL the index holds, whether or not its handler resolved. */
  301. routeCount: number;
  302. /** Rows in `entries` — the ones whose handler the manifest could name. */
  303. shown: number;
  304. truncated: boolean;
  305. topHandlerFile: string | null;
  306. topHandlerFileCount: number;
  307. entries: WireRoute[];
  308. }
  309. /* ---------------------------------------------------------- entry points -- */
  310. export interface WireEntryRoute {
  311. /** The route node's name, verbatim: "POST /v1/users/{id}". */
  312. url: string;
  313. /** The verb, when the name leads with one. Null for a file-routed page. */
  314. method: string | null;
  315. /** The URL without the verb — the same string as `url` when there is none. */
  316. path: string;
  317. handler: string;
  318. handlerKind: string;
  319. /** Where the request is SERVED. */
  320. file: string;
  321. line: number;
  322. handlerId: string | null;
  323. /** Where the URL is REGISTERED — the router file, which is how routes group. */
  324. routeFile: string;
  325. routeLine: number;
  326. routeId: string;
  327. }
  328. export interface WireEntryFile extends WireNodeRef {
  329. /** Calls and instantiations made at the top level of the file. */
  330. calls: number;
  331. /** Distinct other files this one's symbols reach. */
  332. reaches: number;
  333. /** Other files reaching into this one. Zero means nothing imports it. */
  334. dependents: number;
  335. }
  336. export interface WireEntryHub extends WireNodeRef {
  337. dependents: number;
  338. }
  339. export interface WireEntryTest extends WireNodeRef {
  340. /** Distinct other files this test reaches — what it exercises. */
  341. reaches: number;
  342. /** References behind that reach. */
  343. refs: number;
  344. }
  345. export interface WireEntryPoints {
  346. /** Frameworks the resolver detected — named in the Routes header. */
  347. frameworks: string[];
  348. routes: {
  349. routed: boolean;
  350. /** Every `route` node in the graph, resolved handler or not. */
  351. routeCount: number;
  352. items: WireList<WireEntryRoute>;
  353. };
  354. /** `total` is a floor on `files` and `hubs`; on `tests` it is exact. */
  355. files: WireList<WireEntryFile>;
  356. tests: WireList<WireEntryTest>;
  357. hubs: WireList<WireEntryHub>;
  358. index: { lastIndexedAt: number | null; files: number };
  359. timing: { elapsedMs: number; cached: boolean };
  360. }
  361. export interface WireStats {
  362. project: { root: string; name: string };
  363. index: {
  364. state: string | null;
  365. lastIndexedAt: number | null;
  366. stale: boolean;
  367. version: string | null;
  368. extractionVersion: number | null;
  369. backend: string;
  370. journalMode: string;
  371. pendingReferences: number;
  372. generatedFiles: number;
  373. watching: boolean;
  374. watcherDegraded: boolean;
  375. };
  376. graph: {
  377. nodes: number;
  378. edges: number;
  379. files: number;
  380. nodesByKind: Record<string, number>;
  381. edgesByKind: Record<string, number>;
  382. filesByLanguage: Record<string, number>;
  383. dbSizeBytes: number;
  384. walSizeBytes: number;
  385. };
  386. frameworks: string[];
  387. thresholds: { hub: number; uncertainBelow: number };
  388. blastScale: WireBlastScale;
  389. }
  390. /* ------------------------------------------------------------- flow strip -- */
  391. export interface WireFlowEdge extends WireEdge {
  392. /** The link's label: "calls", "via callback · registered at file:line". */
  393. label: string;
  394. /** This hop reads callee → caller — the reader stepped UP into it. */
  395. upward: boolean;
  396. /** Confidence below 0.6: the link is dashed `2 3`. */
  397. uncertain: boolean;
  398. /** A synthesized dynamic-dispatch bridge: dashed `5 3`. */
  399. synthesized: boolean;
  400. }
  401. export interface WireFlowSource {
  402. file: string;
  403. language: string;
  404. from: number;
  405. to: number;
  406. /** Absent when `drift` — a mis-sliced window is worse than an empty card. */
  407. lines?: string[];
  408. highlight?: WireHighlight;
  409. drift: boolean;
  410. reason?: string;
  411. }
  412. /** The call site a card is opened at — the identifier drawn as an accent link. */
  413. export interface WireFlowCallRef {
  414. line: number;
  415. col: number | null;
  416. name: string;
  417. targetId: string;
  418. /** The link points back at the previous card, not on to the next one. */
  419. backwards: boolean;
  420. }
  421. export interface WireFlowHop {
  422. node: WireNodeRef;
  423. /** The edge from the PREVIOUS hop into this one; null on the first. */
  424. edge: WireFlowEdge | null;
  425. callRef: WireFlowCallRef | null;
  426. source: WireFlowSource | null;
  427. }
  428. /** One plausible runtime target of a keyed dispatch — a clickable cap row. */
  429. export interface WireBoundaryCandidate {
  430. node: WireNodeRef;
  431. display: string;
  432. named: boolean;
  433. }
  434. /** A dynamic-dispatch site: the form, the key when it is visible, the targets. */
  435. export interface WireBoundarySite {
  436. form: string;
  437. label: string;
  438. snippet: string;
  439. line: number;
  440. key: string | null;
  441. keyIsType: boolean;
  442. moreSites: number;
  443. candidates: WireBoundaryCandidate[];
  444. candidateNote: string | null;
  445. }
  446. export interface WireFlowContinuation {
  447. node: WireNodeRef;
  448. line: number | null;
  449. confidence: number | null;
  450. }
  451. /** Where the graph stops — the strip's end cap (design spec §3.5). */
  452. export interface WireFlowBoundary {
  453. node: WireNodeRef;
  454. sites: WireBoundarySite[];
  455. uncertain: WireList<WireFlowContinuation>;
  456. further: WireList<WireFlowContinuation>;
  457. missed: WireNodeRef[];
  458. }
  459. export interface WireFlow {
  460. id: string;
  461. /** "execute → rowToFileRecord", for the header's flow picker. */
  462. label: string;
  463. hops: WireFlowHop[];
  464. /** Null on a flow that reaches everything it was asked about. */
  465. boundary: WireFlowBoundary | null;
  466. /** One card at the dispatch site, not a path: the answer ran out here. */
  467. partial: boolean;
  468. }
  469. export interface WireFlowAmbiguity {
  470. token: string;
  471. chosen: WireNodeRef | null;
  472. others: WireNodeRef[];
  473. }
  474. export interface WireFlowPayload {
  475. query: {
  476. kind: 'directed' | 'symbols' | 'trail';
  477. from: string | null;
  478. to: string | null;
  479. symbols: string[];
  480. };
  481. flows: WireFlow[];
  482. ambiguous: WireFlowAmbiguity[];
  483. /** Tokens that named nothing in this index. */
  484. unresolved: string[];
  485. /** Why there is no flow, when there is none. */
  486. reason: string | null;
  487. index: { lastIndexedAt: number | null; edges: number; files: number };
  488. timing: { elapsedMs: number };
  489. }
  490. /* -------------------------------------------------------------- the map -- */
  491. export interface WireMapModule {
  492. /** Directory path, the `(root files)` bucket, or a façade file's own path. */
  493. id: string;
  494. label: string;
  495. files: number;
  496. symbols: number;
  497. languages: Array<{ language: string; files: number }>;
  498. /** More than half its files are tests. */
  499. test: boolean;
  500. /** A single file kept out of the root bucket because it is the façade. */
  501. facade: boolean;
  502. /** Its files, capped — the side panel's list when the module is selected. */
  503. fileList: { total: number; shown: number; truncated: boolean; items: string[] };
  504. }
  505. export interface WireMapLink {
  506. source: string;
  507. target: string;
  508. /** Every confident cross-module edge behind this link. */
  509. count: number;
  510. /**
  511. * The subset resolved through an import, a qualified name, an inheritance
  512. * clause or a typed receiver — what the layering trusts.
  513. */
  514. declared: number;
  515. byKind: Array<{ kind: EdgeKind; count: number }>;
  516. topPairs: Array<{ from: string; to: string; count: number; declared: number }>;
  517. }
  518. export interface WireMapCycle {
  519. size: number;
  520. files: string[];
  521. modules: string[];
  522. }
  523. export interface WireMapPayload {
  524. root: string;
  525. depth: number;
  526. roots: Array<{ root: string; label: string; files: number }>;
  527. modules: WireMapModule[];
  528. links: WireMapLink[];
  529. cycles: { total: number; shown: number; truncated: boolean; items: WireMapCycle[] };
  530. excluded: { uncertainEdges: number; confidenceBelow: number };
  531. index: { lastIndexedAt: number | null; edges: number; files: number };
  532. timing: { elapsedMs: number; cached: boolean };
  533. }