3434import sys
3535import time
3636from collections import defaultdict
37- from dataclasses import dataclass , field , replace
37+ from dataclasses import asdict , dataclass , field , replace
3838from pathlib import Path
3939
4040import kuzu
@@ -215,6 +215,41 @@ class AsyncCallRow:
215215 match : str
216216
217217
218+ @dataclass
219+ class ClientRow :
220+ id : str
221+ client_kind : str
222+ target_service : str
223+ path : str
224+ path_template : str
225+ path_regex : str
226+ method : str
227+ member_fqn : str
228+ member_id : str
229+ microservice : str
230+ module : str
231+ filename : str
232+ start_line : int
233+ end_line : int
234+ resolved : bool
235+ source_layer : str
236+
237+
238+ @dataclass
239+ class DeclaresClientRow :
240+ symbol_id : str
241+ client_id : str
242+ confidence : float
243+ strategy : str
244+
245+
246+ @dataclass
247+ class ClientExtractionStats :
248+ clients_total : int = 0
249+ declares_client_total : int = 0
250+ clients_by_kind : dict [str , int ] = field (default_factory = lambda : defaultdict (int ))
251+
252+
218253@dataclass
219254class CallEdgeStats :
220255 http_calls_total : int = 0
@@ -250,8 +285,11 @@ class GraphTables:
250285 exposes_rows : list [ExposesRow ] = field (default_factory = list )
251286 http_call_rows : list [HttpCallRow ] = field (default_factory = list )
252287 async_call_rows : list [AsyncCallRow ] = field (default_factory = list )
288+ client_rows : list [ClientRow ] = field (default_factory = list )
289+ declares_client_rows : list [DeclaresClientRow ] = field (default_factory = list )
253290 route_stats : RouteExtractionStats = field (default_factory = RouteExtractionStats )
254291 call_edge_stats : CallEdgeStats = field (default_factory = CallEdgeStats )
292+ client_stats : ClientExtractionStats = field (default_factory = ClientExtractionStats )
255293 methods_by_type : dict [str , list [MemberEntry ]] = field (default_factory = dict )
256294 parse_errors : int = 0
257295 skipped_files : int = 0
@@ -1257,6 +1295,26 @@ def _route_id(
12571295 return f"r:{ hashlib .sha1 (key .encode ()).hexdigest ()[:16 ]} "
12581296
12591297
1298+ def _client_id (
1299+ * ,
1300+ microservice : str ,
1301+ member_fqn : str ,
1302+ client_kind : str ,
1303+ path : str ,
1304+ method : str ,
1305+ ) -> str :
1306+ key = f"{ microservice } |{ member_fqn } |{ client_kind } |{ path } |{ method } "
1307+ return f"c:{ hashlib .sha1 (key .encode ()).hexdigest ()[:16 ]} "
1308+
1309+
1310+ def _client_source_layer (strategy : str ) -> str :
1311+ if strategy in {"layer_a_meta" , "layer_b_ann" , "layer_b_fqn" , "layer_c_source" }:
1312+ return strategy
1313+ if strategy != "builtin" :
1314+ log .warning ("unknown client source strategy %r, falling back to builtin" , strategy )
1315+ return "builtin"
1316+
1317+
12601318_ROUTE_LAYER_RANK : dict [str , int ] = {
12611319 "builtin" : 0 ,
12621320 "layer_b_ann" : 1 ,
@@ -1426,6 +1484,8 @@ def pass5_imperative_edges(
14261484 existing_route_ids = set (routes_by_id )
14271485 http_seen : set [tuple [str , str ]] = set ()
14281486 async_seen : set [tuple [str , str ]] = set ()
1487+ client_seen : set [str ] = set ()
1488+ declares_client_seen : set [tuple [str , str ]] = set ()
14291489 route_rows = list (tables .routes_rows )
14301490
14311491 def _micro_factor (member : MemberEntry ) -> float :
@@ -1472,6 +1532,54 @@ def _phantom_async_route_id(call: OutgoingCallDecl) -> str:
14721532 micro_factor = _micro_factor (member )
14731533 for call in final_http_calls + final_async_calls :
14741534 if call .channel == "http" :
1535+ client_path = (call .path_template_call or "" ).strip ()
1536+ client_method = (call .method_call or "" ).strip ().upper ()
1537+ # Keep normalized path fields on Client now so LC3 filter semantics
1538+ # (`path_prefix`) can use persisted columns without extra transforms.
1539+ client_path_template = ""
1540+ client_path_regex = ""
1541+ if client_path :
1542+ client_path_template , client_path_regex = _normalize_path (client_path )
1543+ cid = _client_id (
1544+ microservice = member .microservice ,
1545+ member_fqn = call .method_fqn ,
1546+ client_kind = call .client_kind ,
1547+ path = client_path ,
1548+ method = client_method ,
1549+ )
1550+ if cid not in client_seen :
1551+ client_seen .add (cid )
1552+ tables .client_rows .append (
1553+ ClientRow (
1554+ id = cid ,
1555+ client_kind = call .client_kind ,
1556+ target_service = call .feign_target_name ,
1557+ path = client_path ,
1558+ path_template = client_path_template ,
1559+ path_regex = client_path_regex ,
1560+ method = client_method ,
1561+ member_fqn = call .method_fqn ,
1562+ member_id = member .node_id ,
1563+ microservice = member .microservice ,
1564+ module = member .module ,
1565+ filename = call .filename ,
1566+ start_line = call .start_line ,
1567+ end_line = call .end_line ,
1568+ resolved = call .resolved ,
1569+ source_layer = _client_source_layer (call .resolution_strategy ),
1570+ ),
1571+ )
1572+ dkey = (member .node_id , cid )
1573+ if dkey not in declares_client_seen :
1574+ declares_client_seen .add (dkey )
1575+ tables .declares_client_rows .append (
1576+ DeclaresClientRow (
1577+ symbol_id = member .node_id ,
1578+ client_id = cid ,
1579+ confidence = call .confidence_base ,
1580+ strategy = call .resolution_strategy ,
1581+ ),
1582+ )
14751583 rid = ""
14761584 strategy = call .resolution_strategy
14771585 if call .client_kind == "feign_method" :
@@ -1567,6 +1675,16 @@ def _phantom_async_route_id(call: OutgoingCallDecl) -> str:
15671675 tables .call_edge_stats .async_calls_by_strategy [strategy ] += 1
15681676
15691677 tables .routes_rows = sorted (route_rows , key = lambda r : r .id )
1678+ tables .client_rows = sorted (tables .client_rows , key = lambda c : c .id )
1679+ tables .declares_client_rows = sorted (
1680+ tables .declares_client_rows ,
1681+ key = lambda e : (e .symbol_id , e .client_id ),
1682+ )
1683+ tables .client_stats .clients_total = len (tables .client_rows )
1684+ tables .client_stats .declares_client_total = len (tables .declares_client_rows )
1685+ tables .client_stats .clients_by_kind = defaultdict (int )
1686+ for row in tables .client_rows :
1687+ tables .client_stats .clients_by_kind [row .client_kind ] += 1
15701688 brownfield_strategies = frozenset (
15711689 (
15721690 "layer_b_ann" ,
@@ -1920,6 +2038,9 @@ def _micro_factor(member: MemberEntry | None) -> float:
19202038 "routes_resolved_pct DOUBLE, "
19212039 "routes_from_brownfield_pct DOUBLE, "
19222040 "routes_by_layer STRING, "
2041+ "clients_total INT64, "
2042+ "declares_client_total INT64, "
2043+ "clients_by_kind STRING, "
19232044 "http_calls_total INT64, "
19242045 "async_calls_total INT64, "
19252046 "http_calls_by_strategy STRING, "
@@ -1949,6 +2070,16 @@ def _micro_factor(member: MemberEntry | None) -> float:
19492070 "PRIMARY KEY(id))"
19502071)
19512072
2073+ _SCHEMA_CLIENT = (
2074+ "CREATE NODE TABLE Client("
2075+ "id STRING, client_kind STRING, target_service STRING, "
2076+ "path STRING, path_template STRING, path_regex STRING, method STRING, "
2077+ "member_fqn STRING, member_id STRING, "
2078+ "microservice STRING, module STRING, filename STRING, "
2079+ "start_line INT64, end_line INT64, resolved BOOLEAN, source_layer STRING, "
2080+ "PRIMARY KEY(id))"
2081+ )
2082+
19522083_SCHEMA_EXTENDS = (
19532084 "CREATE REL TABLE EXTENDS(FROM Symbol TO Symbol, "
19542085 "dst_name STRING, dst_fqn STRING, resolved BOOLEAN)"
@@ -1972,6 +2103,10 @@ def _micro_factor(member: MemberEntry | None) -> float:
19722103 "CREATE REL TABLE EXPOSES(FROM Symbol TO Route, "
19732104 "confidence DOUBLE, strategy STRING)"
19742105)
2106+ _SCHEMA_DECLARES_CLIENT = (
2107+ "CREATE REL TABLE DECLARES_CLIENT(FROM Symbol TO Client, "
2108+ "confidence DOUBLE, strategy STRING)"
2109+ )
19752110_SCHEMA_HTTP_CALLS = (
19762111 "CREATE REL TABLE HTTP_CALLS(FROM Symbol TO Route, "
19772112 "confidence DOUBLE, strategy STRING, "
@@ -1986,6 +2121,7 @@ def _micro_factor(member: MemberEntry | None) -> float:
19862121
19872122def _drop_all (conn : kuzu .Connection ) -> None :
19882123 for stmt in (
2124+ "DROP TABLE IF EXISTS DECLARES_CLIENT" ,
19892125 "DROP TABLE IF EXISTS HTTP_CALLS" ,
19902126 "DROP TABLE IF EXISTS ASYNC_CALLS" ,
19912127 "DROP TABLE IF EXISTS EXPOSES" ,
@@ -1996,6 +2132,7 @@ def _drop_all(conn: kuzu.Connection) -> None:
19962132 "DROP TABLE IF EXISTS DECLARES" ,
19972133 "DROP TABLE IF EXISTS Symbol" ,
19982134 "DROP TABLE IF EXISTS Route" ,
2135+ "DROP TABLE IF EXISTS Client" ,
19992136 "DROP TABLE IF EXISTS GraphMeta" ,
20002137 ):
20012138 try :
@@ -2008,13 +2145,15 @@ def _create_schema(conn: kuzu.Connection) -> None:
20082145 for stmt in (
20092146 _SCHEMA_NODE ,
20102147 _SCHEMA_ROUTE ,
2148+ _SCHEMA_CLIENT ,
20112149 _SCHEMA_META ,
20122150 _SCHEMA_EXTENDS ,
20132151 _SCHEMA_IMPLEMENTS ,
20142152 _SCHEMA_INJECTS ,
20152153 _SCHEMA_DECLARES ,
20162154 _SCHEMA_CALLS ,
20172155 _SCHEMA_EXPOSES ,
2156+ _SCHEMA_DECLARES_CLIENT ,
20182157 _SCHEMA_HTTP_CALLS ,
20192158 _SCHEMA_ASYNC_CALLS ,
20202159 ):
@@ -2144,11 +2283,24 @@ def _write_nodes(
21442283 "start_line: $start_line, end_line: $end_line, resolved: $resolved"
21452284 "})"
21462285)
2286+ _CREATE_CLIENT = (
2287+ "CREATE (:Client {"
2288+ "id: $id, client_kind: $client_kind, target_service: $target_service, "
2289+ "path: $path, path_template: $path_template, path_regex: $path_regex, method: $method, "
2290+ "member_fqn: $member_fqn, member_id: $member_id, "
2291+ "microservice: $microservice, module: $module, filename: $filename, "
2292+ "start_line: $start_line, end_line: $end_line, resolved: $resolved, source_layer: $source_layer"
2293+ "})"
2294+ )
21472295
21482296_CREATE_EXPOSES = (
21492297 "MATCH (s:Symbol {id: $sid}), (r:Route {id: $rid}) "
21502298 "CREATE (s)-[:EXPOSES {confidence: $confidence, strategy: $strategy}]->(r)"
21512299)
2300+ _CREATE_DECLARES_CLIENT = (
2301+ "MATCH (s:Symbol {id: $sid}), (c:Client {id: $cid}) "
2302+ "CREATE (s)-[:DECLARES_CLIENT {confidence: $confidence, strategy: $strategy}]->(c)"
2303+ )
21522304_CREATE_HTTP_CALL = (
21532305 "MATCH (s:Symbol {id: $sid}), (r:Route {id: $rid}) "
21542306 "CREATE (s)-[:HTTP_CALLS {confidence: $confidence, strategy: $strategy, "
@@ -2238,6 +2390,15 @@ def _write_routes_and_exposes(conn: kuzu.Connection, tables: GraphTables) -> Non
22382390 "confidence" : row .confidence ,
22392391 "strategy" : row .strategy ,
22402392 })
2393+ for row in tables .client_rows :
2394+ conn .execute (_CREATE_CLIENT , asdict (row ))
2395+ for row in tables .declares_client_rows :
2396+ conn .execute (_CREATE_DECLARES_CLIENT , {
2397+ "sid" : row .symbol_id ,
2398+ "cid" : row .client_id ,
2399+ "confidence" : row .confidence ,
2400+ "strategy" : row .strategy ,
2401+ })
22412402 for row in tables .http_call_rows :
22422403 conn .execute (_CREATE_HTTP_CALL , {
22432404 "sid" : row .symbol_id ,
@@ -2271,6 +2432,7 @@ def _write_meta(conn: kuzu.Connection, tables: GraphTables, source_root: Path) -
22712432 st = tables .route_stats
22722433 routes_fw = dict (sorted (st .by_framework .items ()))
22732434 call_stats = tables .call_edge_stats
2435+ client_stats = tables .client_stats
22742436 http_by_strategy = dict (sorted (call_stats .http_calls_by_strategy .items ()))
22752437 async_by_strategy = dict (sorted (call_stats .async_calls_by_strategy .items ()))
22762438 http_match = dict (sorted (call_stats .http_calls_match_breakdown .items ()))
@@ -2298,16 +2460,21 @@ def _write_meta(conn: kuzu.Connection, tables: GraphTables, source_root: Path) -
22982460 "calls" : calls_unique ,
22992461 "routes" : len (tables .routes_rows ),
23002462 "exposes" : len (tables .exposes_rows ),
2463+ "clients" : len (tables .client_rows ),
2464+ "declares_client" : len (tables .declares_client_rows ),
23012465 "http_calls" : len (tables .http_call_rows ),
23022466 "async_calls" : len (tables .async_call_rows ),
23032467 }
23042468 routes_layer = dict (sorted (st .routes_by_layer .items ()))
2469+ clients_by_kind = dict (sorted (client_stats .clients_by_kind .items ()))
23052470 conn .execute (
23062471 "CREATE (:GraphMeta {key: $k, ontology_version: $ov, built_at: $t, "
23072472 "source_root: $sr, counts_json: $cj, parse_errors: $pe, "
23082473 "routes_total: $routes_total, exposes_total: $exposes_total, "
23092474 "routes_by_framework: $routes_by_framework, routes_resolved_pct: $routes_resolved_pct, "
23102475 "routes_from_brownfield_pct: $routes_from_brownfield_pct, routes_by_layer: $routes_by_layer, "
2476+ "clients_total: $clients_total, declares_client_total: $declares_client_total, "
2477+ "clients_by_kind: $clients_by_kind, "
23112478 "http_calls_total: $http_calls_total, async_calls_total: $async_calls_total, "
23122479 "http_calls_by_strategy: $http_calls_by_strategy, async_calls_by_strategy: $async_calls_by_strategy, "
23132480 "http_calls_resolved_pct: $http_calls_resolved_pct, async_calls_resolved_pct: $async_calls_resolved_pct, "
@@ -2332,6 +2499,9 @@ def _write_meta(conn: kuzu.Connection, tables: GraphTables, source_root: Path) -
23322499 "routes_resolved_pct" : float (st .routes_resolved_pct ),
23332500 "routes_from_brownfield_pct" : float (st .routes_from_brownfield_pct ),
23342501 "routes_by_layer" : json .dumps (routes_layer ),
2502+ "clients_total" : int (client_stats .clients_total ),
2503+ "declares_client_total" : int (client_stats .declares_client_total ),
2504+ "clients_by_kind" : json .dumps (clients_by_kind ),
23352505 "http_calls_total" : call_stats .http_calls_total ,
23362506 "async_calls_total" : call_stats .async_calls_total ,
23372507 "http_calls_by_strategy" : json .dumps (http_by_strategy ),
0 commit comments