2424//! discriminator and emits the inner `HotspotsAttributionResult`
2525//! shape directly (TS contract).
2626
27- use std:: collections:: BTreeMap ;
28-
2927use clap:: Args ;
3028use relayburn_sdk:: {
31- hotspots as sdk_hotspots, ingest_all, normalize_since , AttributionMethod , BashAggregation ,
32- BashVerbAggregation , FileAggregation , HotspotsAttributionResult , HotspotsGroupBy ,
33- HotspotsOptions , HotspotsResult , HotspotsSessionTotal , Ledger , LedgerOpenOptions , Query ,
34- SubagentAggregation ,
29+ hotspots as sdk_hotspots, ingest_all, AttributionMethod , BashAggregation ,
30+ BashVerbAggregation , FileAggregation , HotspotsAttributionResult , HotspotsExcludedBreakdown ,
31+ HotspotsExcludedSourceRow , HotspotsGroupBy , HotspotsOptions , HotspotsResult ,
32+ HotspotsSessionTotal , Ledger , LedgerOpenOptions , SubagentAggregation ,
3533} ;
3634use serde_json:: { json, Map , Value } ;
3735
@@ -148,23 +146,6 @@ fn run_inner(globals: &GlobalArgs, args: HotspotsArgs) -> anyhow::Result<i32> {
148146 . build ( ) ?;
149147 let raw_opts = relayburn_sdk:: RawIngestOptions :: default ( ) ;
150148 rt. block_on ( ingest_all ( handle. raw_mut ( ) , & raw_opts) ) ?;
151-
152- // Pre-compute the per-source coverage-gap breakdown so the human
153- // renderer can name *which* sources contributed excluded turns and
154- // *what* they were missing. Reaching past the SDK verb here is a
155- // small layering compromise: the verb internally walks the same
156- // slice but only surfaces an aggregate `fidelity` block. Doing it
157- // here keeps the source-attributed clause in `coverage_notice` honest
158- // when the slice mixes sources (e.g. excluded codex + opencode turns
159- // both showing up in the message).
160- let mut q = Query :: default ( ) ;
161- if let Some ( p) = args. project . clone ( ) {
162- q. project = Some ( p) ;
163- }
164- if let Some ( since) = normalize_since ( args. since . as_deref ( ) ) ? {
165- q. since = Some ( since) ;
166- }
167- let breakdown = describe_excluded_turns ( & handle, & q) ?;
168149 drop ( handle) ;
169150
170151 let result = sdk_hotspots ( HotspotsOptions {
@@ -181,72 +162,10 @@ fn run_inner(globals: &GlobalArgs, args: HotspotsArgs) -> anyhow::Result<i32> {
181162 return Ok ( 0 ) ;
182163 }
183164 let limit = if args. all { usize:: MAX } else { DEFAULT_TOP_N } ;
184- emit_human ( & result, limit, & breakdown ) ;
165+ emit_human ( & result, limit) ;
185166 Ok ( 0 )
186167}
187168
188- /// Per-source coverage-gap breakdown, mirroring the TS
189- /// `describeExcluded` helper. Only counts turns that *fail* the
190- /// hotspots-attribution gate (`hasToolCalls && hasToolResultEvents`);
191- /// turns without a `fidelity` field are best-effort full and never
192- /// excluded. The SDK's [`relayburn_sdk::HotspotsResult`] reports
193- /// aggregate analyzed/excluded counts but not the source mix; we walk
194- /// the ledger slice ourselves to recover that detail.
195- #[ derive( Debug , Clone , Default ) ]
196- struct CoverageGapBreakdown {
197- /// Sorted by source label so the rendered clause is deterministic.
198- sources : BTreeMap < String , SourceClause > ,
199- /// Total excluded count across all sources. Equal to
200- /// `attribution_result.fidelity.excluded` for non-empty slices.
201- excluded : u64 ,
202- /// Total analyzed (eligible) count.
203- analyzed : u64 ,
204- }
205-
206- #[ derive( Debug , Clone , Default ) ]
207- struct SourceClause {
208- /// Distinct missing-coverage labels (`tool-call records`, etc.).
209- missing : BTreeMap < String , ( ) > ,
210- /// Distinct granularities observed on excluded turns from this
211- /// source. Used by the inline rendered form `<missing>, <gran>
212- /// granularity (<source>)`.
213- granularities : BTreeMap < String , ( ) > ,
214- count : u64 ,
215- }
216-
217- fn describe_excluded_turns (
218- handle : & relayburn_sdk:: LedgerHandle ,
219- q : & Query ,
220- ) -> anyhow:: Result < CoverageGapBreakdown > {
221- let mut out = CoverageGapBreakdown :: default ( ) ;
222- for enriched in handle. raw ( ) . query_turns ( q) ? {
223- let t = & enriched. turn ;
224- let Some ( f) = t. fidelity . as_ref ( ) else {
225- // No fidelity → treat as best-effort full; never excluded.
226- out. analyzed += 1 ;
227- continue ;
228- } ;
229- let passes = f. coverage . has_tool_calls && f. coverage . has_tool_result_events ;
230- if passes {
231- out. analyzed += 1 ;
232- continue ;
233- }
234- out. excluded += 1 ;
235- let entry = out. sources . entry ( t. source . wire_str ( ) . to_string ( ) ) . or_default ( ) ;
236- entry. count += 1 ;
237- if !f. coverage . has_tool_calls {
238- entry. missing . insert ( "tool-call records" . to_string ( ) , ( ) ) ;
239- }
240- if !f. coverage . has_tool_result_events {
241- entry. missing . insert ( "tool-result events" . to_string ( ) , ( ) ) ;
242- }
243- entry
244- . granularities
245- . insert ( f. granularity . wire_str ( ) . to_string ( ) , ( ) ) ;
246- }
247- Ok ( out)
248- }
249-
250169fn emit_json ( result : & HotspotsResult ) {
251170 let mut value = hotspots_result_to_json ( result) ;
252171 coerce_whole_f64_to_int ( & mut value) ;
@@ -492,9 +411,9 @@ fn subagent_to_json(s: &SubagentAggregation) -> Value {
492411
493412// ---------- human rendering ----------
494413
495- fn emit_human ( result : & HotspotsResult , limit : usize , breakdown : & CoverageGapBreakdown ) {
414+ fn emit_human ( result : & HotspotsResult , limit : usize ) {
496415 match result {
497- HotspotsResult :: Attribution ( a) => emit_human_attribution ( a, limit, breakdown ) ,
416+ HotspotsResult :: Attribution ( a) => emit_human_attribution ( a, limit) ,
498417 // The single-axis group_by surfaces aren't yet tied to a golden
499418 // snapshot (the snapshot covers the default attribution view),
500419 // so we render their tables on a best-effort basis with the same
@@ -608,17 +527,13 @@ where
608527 print ! ( "{}" , lines. join( "\n " ) ) ;
609528}
610529
611- fn emit_human_attribution (
612- a : & HotspotsAttributionResult ,
613- limit : usize ,
614- breakdown : & CoverageGapBreakdown ,
615- ) {
530+ fn emit_human_attribution ( a : & HotspotsAttributionResult , limit : usize ) {
616531 let degraded = a. attribution_degraded ;
617532 let approx_suffix = if degraded { " (approximate)" } else { "" } ;
618533 let mut out: Vec < String > = Vec :: new ( ) ;
619534 out. push ( String :: new ( ) ) ;
620535 out. push ( format ! ( "turns analyzed: {}" , format_uint( a. turns_analyzed) ) ) ;
621- if let Some ( notice) = coverage_notice ( a, breakdown ) {
536+ if let Some ( notice) = coverage_notice ( a) {
622537 out. push ( notice) ;
623538 }
624539 out. push ( format ! (
@@ -773,10 +688,7 @@ fn emit_human_attribution(
773688 print ! ( "{}" , out. join( "\n " ) ) ;
774689}
775690
776- fn coverage_notice (
777- a : & HotspotsAttributionResult ,
778- breakdown : & CoverageGapBreakdown ,
779- ) -> Option < String > {
691+ fn coverage_notice ( a : & HotspotsAttributionResult ) -> Option < String > {
780692 let analyzed = a. fidelity . analyzed ;
781693 let excluded = a. fidelity . excluded ;
782694 if excluded == 0 {
@@ -786,16 +698,14 @@ fn coverage_notice(
786698 // The TS shape is one inline clause per source kind, joined with " and ".
787699 // Each clause names the missing field(s) + the granularity bucket the
788700 // excluded turns carried, with the source name in parens. Sources are
789- // walked in BTreeMap order for stable rendering.
790- let clauses: Vec < String > = breakdown
791- . sources
792- . iter ( )
793- . map ( |( source, row) | render_inline_source_clause ( source, row) )
794- . collect ( ) ;
701+ // walked in BTreeMap order for stable rendering. The breakdown is
702+ // computed by the SDK in the same pass that produced the rest of the
703+ // attribution result — no second ledger walk here.
704+ let clauses: Vec < String > = render_source_clauses ( & a. fidelity . excluded_by_source ) ;
795705 let suffix = if clauses. is_empty ( ) {
796- // Fall back to the SDK's aggregate counts if for some reason the
797- // re-walk surfaced no breakdown (e.g. record without fidelity but
798- // the SDK still excluded it). Don't fabricate a source label.
706+ // Fall back to the SDK's aggregate counts if the breakdown is empty
707+ // (e.g. a turn without a fidelity record that the SDK still excluded).
708+ // Don't fabricate a source label.
799709 String :: new ( )
800710 } else {
801711 format ! ( " for {}" , clauses. join( " and " ) )
@@ -809,14 +719,22 @@ fn coverage_notice(
809719 ) )
810720}
811721
812- fn render_inline_source_clause ( source : & str , row : & SourceClause ) -> String {
722+ fn render_source_clauses ( breakdown : & HotspotsExcludedBreakdown ) -> Vec < String > {
723+ breakdown
724+ . sources
725+ . iter ( )
726+ . map ( |( source, row) | render_inline_source_clause ( source, row) )
727+ . collect ( )
728+ }
729+
730+ fn render_inline_source_clause ( source : & str , row : & HotspotsExcludedSourceRow ) -> String {
813731 let mut inner: Vec < String > = Vec :: new ( ) ;
814732 if !row. missing . is_empty ( ) {
815- let missing: Vec < & str > = row. missing . keys ( ) . map ( String :: as_str) . collect ( ) ;
733+ let missing: Vec < & str > = row. missing . iter ( ) . map ( String :: as_str) . collect ( ) ;
816734 inner. push ( format ! ( "missing {}" , missing. join( ", " ) ) ) ;
817735 }
818736 if !row. granularities . is_empty ( ) {
819- let grans: Vec < & str > = row. granularities . keys ( ) . map ( String :: as_str) . collect ( ) ;
737+ let grans: Vec < & str > = row. granularities . iter ( ) . map ( String :: as_str) . collect ( ) ;
820738 inner. push ( format ! ( "{} granularity" , grans. join( "+" ) ) ) ;
821739 }
822740 if inner. is_empty ( ) {
0 commit comments