@@ -68,44 +68,15 @@ def _health_label(metrics: dict) -> str:
6868# ---------------------------------------------------------------------------
6969
7070
71- def collect_symbol_metrics (
72- conn : sqlite3 .Connection ,
73- symbol_id : int ,
74- * ,
75- include_comprehension : bool = True ,
76- ) -> dict :
77- """Gather all available metrics for a single symbol.
71+ def _swallow (tag : str , exc : BaseException ) -> None :
72+ """Centralised swallowed-exception logger to keep the helpers tight."""
73+ from roam .observability import log_swallowed
74+
75+ log_swallowed (tag , exc )
7876
79- Returns a flat dict with keys: complexity, fan_in, fan_out, pagerank,
80- betweenness, churn, commits, test_files, layer_depth, dead_code_risk,
81- loc, co_change_count.
82- """
83- result : dict = {
84- "complexity" : 0 ,
85- "fan_in" : 0 ,
86- "fan_out" : 0 ,
87- "pagerank" : 0.0 ,
88- "betweenness" : 0.0 ,
89- "closeness" : 0.0 ,
90- "eigenvector" : 0.0 ,
91- "clustering_coefficient" : 0.0 ,
92- "debt_score" : 0.0 ,
93- "churn" : 0 ,
94- "commits" : 0 ,
95- "test_files" : 0 ,
96- "layer_depth" : None ,
97- "dead_code_risk" : False ,
98- "loc" : 0 ,
99- "co_change_count" : 0 ,
100- "information_scatter" : 0 ,
101- "working_set_size" : 0 ,
102- "comprehension_difficulty" : 0.0 ,
103- "coverage_pct" : None ,
104- "covered_lines" : 0 ,
105- "coverable_lines" : 0 ,
106- }
10777
108- # -- symbol_metrics (cognitive complexity, line_count) --
78+ def _populate_symbol_metrics (conn , symbol_id : int , result : dict ) -> None :
79+ """Pull cognitive complexity + LOC + (optional) coverage columns."""
10980 try :
11081 sm = conn .execute (
11182 "SELECT cognitive_complexity, line_count FROM symbol_metrics WHERE symbol_id = ?" ,
@@ -114,11 +85,8 @@ def collect_symbol_metrics(
11485 if sm :
11586 result ["complexity" ] = sm ["cognitive_complexity" ] or 0
11687 result ["loc" ] = sm ["line_count" ] or 0
117- except Exception as _exc : # noqa: BLE001 — defensive
118- from roam .observability import log_swallowed
119-
120- log_swallowed ("cmd_metrics:metric_query" , _exc )
121- # Optional imported coverage columns (safe on older DB schemas)
88+ except Exception as exc : # noqa: BLE001 — defensive
89+ _swallow ("cmd_metrics:metric_query" , exc )
12290 try :
12391 cov = conn .execute (
12492 "SELECT coverage_pct, covered_lines, coverable_lines FROM symbol_metrics WHERE symbol_id = ?" ,
@@ -128,12 +96,12 @@ def collect_symbol_metrics(
12896 result ["coverage_pct" ] = cov ["coverage_pct" ]
12997 result ["covered_lines" ] = cov ["covered_lines" ] or 0
13098 result ["coverable_lines" ] = cov ["coverable_lines" ] or 0
131- except Exception as _exc : # noqa: BLE001 — defensive
132- from roam . observability import log_swallowed
99+ except Exception as exc : # noqa: BLE001 — defensive
100+ _swallow ( "cmd_metrics:metric_query" , exc )
133101
134- log_swallowed ("cmd_metrics:metric_query" , _exc )
135102
136- # -- graph_metrics (pagerank, in_degree, out_degree, betweenness) --
103+ def _populate_graph_metrics (conn , symbol_id : int , result : dict ) -> None :
104+ """Pull pagerank/fan-in/fan-out/betweenness + (optional) SNA-v2 cols."""
137105 try :
138106 gm = conn .execute (
139107 "SELECT pagerank, in_degree, out_degree, betweenness FROM graph_metrics WHERE symbol_id = ?" ,
@@ -144,11 +112,8 @@ def collect_symbol_metrics(
144112 result ["fan_in" ] = gm ["in_degree" ] or 0
145113 result ["fan_out" ] = gm ["out_degree" ] or 0
146114 result ["betweenness" ] = gm ["betweenness" ] or 0.0
147- except Exception as _exc : # noqa: BLE001 — defensive
148- from roam .observability import log_swallowed
149-
150- log_swallowed ("cmd_metrics:metric_query" , _exc )
151- # Optional SNA v2 columns (safe on older DB schemas)
115+ except Exception as exc : # noqa: BLE001 — defensive
116+ _swallow ("cmd_metrics:metric_query" , exc )
152117 try :
153118 extra = conn .execute (
154119 "SELECT closeness, eigenvector, clustering_coefficient, debt_score FROM graph_metrics WHERE symbol_id = ?" ,
@@ -159,89 +124,118 @@ def collect_symbol_metrics(
159124 result ["eigenvector" ] = extra ["eigenvector" ] or 0.0
160125 result ["clustering_coefficient" ] = extra ["clustering_coefficient" ] or 0.0
161126 result ["debt_score" ] = extra ["debt_score" ] or 0.0
162- except Exception as _exc : # noqa: BLE001 — defensive
163- from roam .observability import log_swallowed
127+ except Exception as exc : # noqa: BLE001 — defensive
128+ _swallow ("cmd_metrics:metric_query" , exc )
129+
130+
131+ def _populate_edge_fanout_fallback (conn , symbol_id : int , result : dict ) -> None :
132+ """When graph_metrics returned 0/0, fall back to raw edge counts."""
133+ if result ["fan_in" ] != 0 or result ["fan_out" ] != 0 :
134+ return
135+ try :
136+ fi = conn .execute ("SELECT COUNT(*) FROM edges WHERE target_id = ?" , (symbol_id ,)).fetchone ()
137+ fo = conn .execute ("SELECT COUNT(*) FROM edges WHERE source_id = ?" , (symbol_id ,)).fetchone ()
138+ result ["fan_in" ] = fi [0 ] if fi else 0
139+ result ["fan_out" ] = fo [0 ] if fo else 0
140+ except Exception as exc : # noqa: BLE001 — defensive
141+ _swallow ("cmd_metrics:nested_query" , exc )
164142
165- log_swallowed ("cmd_metrics:metric_query" , _exc )
166143
167- # -- edges (fallback fan-in / fan-out from raw edges) --
168- if result ["fan_in" ] == 0 and result ["fan_out" ] == 0 :
169- try :
170- fi = conn .execute (
171- "SELECT COUNT(*) FROM edges WHERE target_id = ?" ,
172- (symbol_id ,),
173- ).fetchone ()
174- fo = conn .execute (
175- "SELECT COUNT(*) FROM edges WHERE source_id = ?" ,
176- (symbol_id ,),
177- ).fetchone ()
178- result ["fan_in" ] = fi [0 ] if fi else 0
179- result ["fan_out" ] = fo [0 ] if fo else 0
180- except Exception as _exc : # noqa: BLE001 — defensive
181- from roam .observability import log_swallowed
182-
183- log_swallowed ("cmd_metrics:nested_query" , _exc )
184-
185- # -- dead_code_risk: fan_in == 0 for non-entry-point symbols --
144+ def _populate_file_level_metrics (conn , file_id : int , result : dict ) -> None :
145+ """Pull churn/commits, test-file count, and co-change count for a file."""
146+ try :
147+ fs = conn .execute (
148+ "SELECT commit_count, total_churn FROM file_stats WHERE file_id = ?" ,
149+ (file_id ,),
150+ ).fetchone ()
151+ if fs :
152+ result ["commits" ] = fs ["commit_count" ] or 0
153+ result ["churn" ] = fs ["total_churn" ] or 0
154+ except Exception as exc : # noqa: BLE001 — defensive
155+ _swallow ("cmd_metrics:nested_query" , exc )
156+ try :
157+ tf = conn .execute (
158+ "SELECT COUNT(DISTINCT fe.source_file_id) "
159+ "FROM file_edges fe "
160+ "JOIN files f ON fe.source_file_id = f.id "
161+ "WHERE fe.target_file_id = ? AND f.file_role = 'test'" ,
162+ (file_id ,),
163+ ).fetchone ()
164+ result ["test_files" ] = tf [0 ] if tf else 0
165+ except Exception as exc : # noqa: BLE001 — defensive
166+ _swallow ("cmd_metrics:nested_query" , exc )
167+ try :
168+ cc_row = conn .execute (
169+ "SELECT SUM(cochange_count) AS total FROM git_cochange WHERE file_id_a = ? OR file_id_b = ?" ,
170+ (file_id , file_id ),
171+ ).fetchone ()
172+ result ["co_change_count" ] = cc_row ["total" ] or 0 if cc_row else 0
173+ except Exception as exc : # noqa: BLE001 — defensive
174+ _swallow ("cmd_metrics:nested_query" , exc )
175+
176+
177+ def _populate_dead_code_risk (sym_row , result : dict ) -> None :
178+ """Mark symbols with zero fan-in as dead-code risks unless they're
179+ exported (entry points)."""
180+ if result ["fan_in" ] != 0 :
181+ return
182+ kind = sym_row ["kind" ] or ""
183+ if kind in ("function" , "method" , "class" ) and not sym_row ["is_exported" ]:
184+ result ["dead_code_risk" ] = True
185+
186+
187+ def collect_symbol_metrics (
188+ conn : sqlite3 .Connection ,
189+ symbol_id : int ,
190+ * ,
191+ include_comprehension : bool = True ,
192+ ) -> dict :
193+ """Gather all available metrics for a single symbol.
194+
195+ Returns a flat dict with keys: complexity, fan_in, fan_out, pagerank,
196+ betweenness, churn, commits, test_files, layer_depth, dead_code_risk,
197+ loc, co_change_count.
198+ """
199+ result : dict = {
200+ "complexity" : 0 ,
201+ "fan_in" : 0 ,
202+ "fan_out" : 0 ,
203+ "pagerank" : 0.0 ,
204+ "betweenness" : 0.0 ,
205+ "closeness" : 0.0 ,
206+ "eigenvector" : 0.0 ,
207+ "clustering_coefficient" : 0.0 ,
208+ "debt_score" : 0.0 ,
209+ "churn" : 0 ,
210+ "commits" : 0 ,
211+ "test_files" : 0 ,
212+ "layer_depth" : None ,
213+ "dead_code_risk" : False ,
214+ "loc" : 0 ,
215+ "co_change_count" : 0 ,
216+ "information_scatter" : 0 ,
217+ "working_set_size" : 0 ,
218+ "comprehension_difficulty" : 0.0 ,
219+ "coverage_pct" : None ,
220+ "covered_lines" : 0 ,
221+ "coverable_lines" : 0 ,
222+ }
223+
224+ _populate_symbol_metrics (conn , symbol_id , result )
225+ _populate_graph_metrics (conn , symbol_id , result )
226+ _populate_edge_fanout_fallback (conn , symbol_id , result )
227+
186228 sym_row = conn .execute (
187229 "SELECT kind, is_exported, file_id FROM symbols WHERE id = ?" ,
188230 (symbol_id ,),
189231 ).fetchone ()
190232 if sym_row :
191- kind = sym_row ["kind" ] or ""
192- is_exported = sym_row ["is_exported" ]
193- if result ["fan_in" ] == 0 and kind in ("function" , "method" , "class" ):
194- # Entry points (exported, main, __init__) are not dead code
195- if not is_exported :
196- result ["dead_code_risk" ] = True
197-
198- # -- churn / commits from git_file_stats via file_id --
199- file_id = sym_row ["file_id" ]
200- try :
201- fs = conn .execute (
202- "SELECT commit_count, total_churn FROM file_stats WHERE file_id = ?" ,
203- (file_id ,),
204- ).fetchone ()
205- if fs :
206- result ["commits" ] = fs ["commit_count" ] or 0
207- result ["churn" ] = fs ["total_churn" ] or 0
208- except Exception as _exc : # noqa: BLE001 — defensive
209- from roam .observability import log_swallowed
210-
211- log_swallowed ("cmd_metrics:nested_query" , _exc )
212-
213- # -- test files: count files with file_role='test' that reference
214- # the same file via file_edges --
215- try :
216- tf = conn .execute (
217- "SELECT COUNT(DISTINCT fe.source_file_id) "
218- "FROM file_edges fe "
219- "JOIN files f ON fe.source_file_id = f.id "
220- "WHERE fe.target_file_id = ? AND f.file_role = 'test'" ,
221- (file_id ,),
222- ).fetchone ()
223- result ["test_files" ] = tf [0 ] if tf else 0
224- except Exception as _exc : # noqa: BLE001 — defensive
225- from roam .observability import log_swallowed
226-
227- log_swallowed ("cmd_metrics:nested_query" , _exc )
228-
229- # -- co_change_count --
230- try :
231- cc_row = conn .execute (
232- "SELECT SUM(cochange_count) AS total FROM git_cochange WHERE file_id_a = ? OR file_id_b = ?" ,
233- (file_id , file_id ),
234- ).fetchone ()
235- result ["co_change_count" ] = cc_row ["total" ] or 0 if cc_row else 0
236- except Exception as _exc : # noqa: BLE001 — defensive
237- from roam .observability import log_swallowed
238-
239- log_swallowed ("cmd_metrics:nested_query" , _exc )
240-
241- # Comprehension difficulty metrics (#71):
242- # - information scatter: distinct files in 2-hop closure
243- # - working set size: symbols in 2-hop closure
244- # - composite score from fan-out, scatter, working set, complexity
233+ _populate_dead_code_risk (sym_row , result )
234+ _populate_file_level_metrics (conn , sym_row ["file_id" ], result )
235+
236+ # Comprehension difficulty metrics (#71): information scatter
237+ # (distinct files in 2-hop closure) + working set size + composite
238+ # score from fan-out, scatter, working set, complexity.
245239 if include_comprehension :
246240 scatter , working_set = _comprehension_neighborhood (conn , symbol_id )
247241 result ["information_scatter" ] = scatter
0 commit comments