@@ -19,6 +19,7 @@ const FEEDBACK_LEARNING_REJECT_TAGS = new Set([
1919 'semantic-feedback:rejected' ,
2020] )
2121
22+ const CONTEXT_SOURCE_TAG_PREFIX = 'context-source:'
2223const PATTERN_REPOSITORY_TAG_PREFIX = 'pattern-repository:'
2324
2425function isLabeledFeedbackComment ( comment : ReviewComment ) : boolean {
@@ -58,6 +59,58 @@ function isPatternRepositoryComment(comment: ReviewComment): boolean {
5859 return extractPatternRepositorySources ( comment ) . length > 0
5960}
6061
62+ function extractContextSources ( comment : ReviewComment ) : string [ ] {
63+ const explicit = comment . tags
64+ . filter ( tag => tag . startsWith ( CONTEXT_SOURCE_TAG_PREFIX ) )
65+ . map ( tag => tag . slice ( CONTEXT_SOURCE_TAG_PREFIX . length ) )
66+ . filter ( Boolean )
67+
68+ if ( explicit . length > 0 ) {
69+ return Array . from ( new Set ( explicit ) )
70+ }
71+
72+ return Array . from ( new Set (
73+ extractPatternRepositorySources ( comment ) . map ( source => `pattern-repository:${ source } ` ) ,
74+ ) )
75+ }
76+
77+ function isContextSourceComment ( comment : ReviewComment ) : boolean {
78+ return extractContextSources ( comment ) . length > 0
79+ }
80+
81+ function formatContextSourceName ( name : string ) : string {
82+ if ( name . startsWith ( 'pattern-repository:' ) ) {
83+ const source = name . slice ( 'pattern-repository:' . length ) || 'external'
84+ return `Pattern repository · ${ source } `
85+ }
86+
87+ const knownLabels : Record < string , string > = {
88+ 'custom-context' : 'Custom context' ,
89+ 'dependency-graph' : 'Dependency graph' ,
90+ 'jira-issue' : 'Jira issue' ,
91+ 'linear-issue' : 'Linear issue' ,
92+ 'path-focus' : 'Path focus' ,
93+ 'related-test-file' : 'Related test file' ,
94+ 'repository-graph' : 'Repository graph' ,
95+ 'reverse-dependency-summary' : 'Reverse dependency summary' ,
96+ 'semantic-retrieval' : 'Semantic retrieval' ,
97+ 'similar-implementation' : 'Similar implementation' ,
98+ 'symbol-graph' : 'Symbol graph' ,
99+ }
100+
101+ if ( knownLabels [ name ] ) {
102+ return knownLabels [ name ]
103+ }
104+
105+ return name
106+ . split ( ':' )
107+ . map ( part => part
108+ . split ( '-' )
109+ . map ( token => token ? token [ 0 ] . toUpperCase ( ) + token . slice ( 1 ) : token )
110+ . join ( ' ' ) )
111+ . join ( ' · ' )
112+ }
113+
61114export function computeAnalytics ( reviews : ReviewSession [ ] ) {
62115 const completed = getCompletedReviews ( reviews )
63116
@@ -242,6 +295,90 @@ export function computeAnalytics(reviews: ReviewSession[]) {
242295 } ) )
243296 . sort ( ( left , right ) => right . total - left . total || right . accepted - left . accepted )
244297
298+ const contextSourceTotals : Record < string , {
299+ total : number
300+ labeled : number
301+ accepted : number
302+ rejected : number
303+ resolved : number
304+ reviewIds : Set < string >
305+ } > = { }
306+
307+ const contextSourceSeries = completed . map ( ( r , i ) => {
308+ const contextSourceComments = r . comments . filter ( isContextSourceComment )
309+ const labeledContextSourceComments = contextSourceComments . filter ( isLabeledFeedbackComment )
310+ const accepted = labeledContextSourceComments . filter ( comment => comment . feedback === 'accept' ) . length
311+ const rejected = labeledContextSourceComments . filter ( comment => comment . feedback === 'reject' ) . length
312+ const resolved = contextSourceComments . filter ( comment => comment . status === 'Resolved' ) . length
313+ const reviewSources = new Set < string > ( )
314+
315+ for ( const comment of contextSourceComments ) {
316+ const sources = Array . from ( new Set ( extractContextSources ( comment ) ) )
317+ const isLabeled = isLabeledFeedbackComment ( comment )
318+ const isResolved = comment . status === 'Resolved'
319+
320+ for ( const source of sources ) {
321+ reviewSources . add ( source )
322+ const current = contextSourceTotals [ source ] ?? {
323+ total : 0 ,
324+ labeled : 0 ,
325+ accepted : 0 ,
326+ rejected : 0 ,
327+ resolved : 0 ,
328+ reviewIds : new Set < string > ( ) ,
329+ }
330+
331+ current . total += 1
332+ if ( isLabeled ) {
333+ current . labeled += 1
334+ if ( comment . feedback === 'accept' ) {
335+ current . accepted += 1
336+ } else if ( comment . feedback === 'reject' ) {
337+ current . rejected += 1
338+ }
339+ }
340+ if ( isResolved ) {
341+ current . resolved += 1
342+ }
343+ current . reviewIds . add ( r . id )
344+ contextSourceTotals [ source ] = current
345+ }
346+ }
347+
348+ return {
349+ reviewId : r . id ,
350+ idx : i + 1 ,
351+ label : `#${ i + 1 } ` ,
352+ findings : contextSourceComments . length ,
353+ labeled : labeledContextSourceComments . length ,
354+ accepted,
355+ rejected,
356+ resolved,
357+ sourceCount : reviewSources . size ,
358+ acceptanceRate : labeledContextSourceComments . length > 0
359+ ? accepted / labeledContextSourceComments . length
360+ : null ,
361+ fixRate : contextSourceComments . length > 0
362+ ? resolved / contextSourceComments . length
363+ : null ,
364+ }
365+ } )
366+
367+ const contextSourceData = Object . entries ( contextSourceTotals )
368+ . map ( ( [ name , totals ] ) => ( {
369+ name,
370+ label : formatContextSourceName ( name ) ,
371+ total : totals . total ,
372+ labeled : totals . labeled ,
373+ accepted : totals . accepted ,
374+ rejected : totals . rejected ,
375+ resolved : totals . resolved ,
376+ reviewCount : totals . reviewIds . size ,
377+ acceptanceRate : totals . labeled > 0 ? totals . accepted / totals . labeled : 0 ,
378+ fixRate : totals . total > 0 ? totals . resolved / totals . total : 0 ,
379+ } ) )
380+ . sort ( ( left , right ) => right . total - left . total || right . accepted - left . accepted )
381+
245382 const feedbackCategoryData = Object . entries ( feedbackTotalsByCategory )
246383 . map ( ( [ name , totals ] ) => {
247384 const total = totals . accepted + totals . rejected
@@ -458,6 +595,57 @@ export function computeAnalytics(reviews: ReviewSession[]) {
458595 && patternRepositoryLabeledTotal > 0
459596 ? patternRepositoryAcceptanceRate - patternRepositoryBaselineAcceptanceRate
460597 : null
598+ const contextSourceFindingTotal = contextSourceSeries . reduce (
599+ ( sum , point ) => sum + point . findings ,
600+ 0 ,
601+ )
602+ const contextSourceLabeledTotal = contextSourceSeries . reduce (
603+ ( sum , point ) => sum + point . labeled ,
604+ 0 ,
605+ )
606+ const contextSourceAcceptedTotal = contextSourceSeries . reduce (
607+ ( sum , point ) => sum + point . accepted ,
608+ 0 ,
609+ )
610+ const contextSourceRejectedTotal = contextSourceSeries . reduce (
611+ ( sum , point ) => sum + point . rejected ,
612+ 0 ,
613+ )
614+ const contextSourceResolvedTotal = contextSourceSeries . reduce (
615+ ( sum , point ) => sum + point . resolved ,
616+ 0 ,
617+ )
618+ const contextSourceReviewCount = contextSourceSeries . filter (
619+ point => point . findings > 0 ,
620+ ) . length
621+ const contextSourceSourceCount = contextSourceData . length
622+ const contextSourceUtilizationRate = completed . length > 0
623+ ? contextSourceReviewCount / completed . length
624+ : 0
625+ const contextSourceAcceptanceRate = contextSourceLabeledTotal > 0
626+ ? contextSourceAcceptedTotal / contextSourceLabeledTotal
627+ : 0
628+ const contextSourceBaselineLabeledTotal = labeledFeedbackTotal - contextSourceLabeledTotal
629+ const contextSourceBaselineAcceptedTotal = acceptedFeedbackTotal - contextSourceAcceptedTotal
630+ const contextSourceBaselineAcceptanceRate = contextSourceBaselineLabeledTotal > 0
631+ ? contextSourceBaselineAcceptedTotal / contextSourceBaselineLabeledTotal
632+ : null
633+ const contextSourceAcceptanceLift = contextSourceBaselineAcceptanceRate != null
634+ && contextSourceLabeledTotal > 0
635+ ? contextSourceAcceptanceRate - contextSourceBaselineAcceptanceRate
636+ : null
637+ const contextSourceFixRate = contextSourceFindingTotal > 0
638+ ? contextSourceResolvedTotal / contextSourceFindingTotal
639+ : 0
640+ const contextSourceBaselineFindingTotal = totalCommentCount - contextSourceFindingTotal
641+ const contextSourceBaselineResolvedTotal = totalResolvedComments - contextSourceResolvedTotal
642+ const contextSourceBaselineFixRate = contextSourceBaselineFindingTotal > 0
643+ ? contextSourceBaselineResolvedTotal / contextSourceBaselineFindingTotal
644+ : null
645+ const contextSourceFixLift = contextSourceBaselineFixRate != null
646+ && contextSourceFindingTotal > 0
647+ ? contextSourceFixRate - contextSourceBaselineFixRate
648+ : null
461649
462650 const sevTotals : Record < Severity , number > = { Error : 0 , Warning : 0 , Info : 0 , Suggestion : 0 }
463651 for ( const r of completed ) {
@@ -480,6 +668,8 @@ export function computeAnalytics(reviews: ReviewSession[]) {
480668 feedbackLearningSeries,
481669 patternRepositorySeries,
482670 patternRepositorySourceData,
671+ contextSourceSeries,
672+ contextSourceData,
483673 topAcceptedCategories,
484674 topRejectedCategories,
485675 topAcceptedRules,
@@ -535,6 +725,21 @@ export function computeAnalytics(reviews: ReviewSession[]) {
535725 patternRepositoryBaselineLabeledTotal,
536726 patternRepositoryBaselineAcceptanceRate,
537727 patternRepositoryAcceptanceLift,
728+ contextSourceFindingTotal,
729+ contextSourceLabeledTotal,
730+ contextSourceAcceptedTotal,
731+ contextSourceRejectedTotal,
732+ contextSourceResolvedTotal,
733+ contextSourceReviewCount,
734+ contextSourceSourceCount,
735+ contextSourceUtilizationRate,
736+ contextSourceAcceptanceRate,
737+ contextSourceBaselineLabeledTotal,
738+ contextSourceBaselineAcceptanceRate,
739+ contextSourceAcceptanceLift,
740+ contextSourceFixRate,
741+ contextSourceBaselineFixRate,
742+ contextSourceFixLift,
538743 } ,
539744 }
540745}
@@ -543,6 +748,7 @@ export type AnalyticsDrilldownSelection =
543748 | { type : 'review' ; reviewId : string }
544749 | { type : 'category' ; category : string }
545750 | { type : 'rule' ; ruleId : string }
751+ | { type : 'contextSource' ; source : string }
546752 | { type : 'patternRepositorySource' ; source : string }
547753
548754export interface AnalyticsDrilldown {
@@ -622,6 +828,9 @@ export function buildAnalyticsDrilldown(
622828 if ( selection . type === 'rule' ) {
623829 return comment . rule_id ?. trim ( ) === selection . ruleId
624830 }
831+ if ( selection . type === 'contextSource' ) {
832+ return extractContextSources ( comment ) . includes ( selection . source )
833+ }
625834 return extractPatternRepositorySources ( comment ) . includes ( selection . source )
626835 } )
627836 . map ( comment => ( { review, label, comment } ) ) )
@@ -654,6 +863,8 @@ export function buildAnalyticsDrilldown(
654863 ? `Category · ${ selection . category } `
655864 : selection . type === 'rule'
656865 ? `Rule · ${ selection . ruleId } `
866+ : selection . type === 'contextSource'
867+ ? `Context source · ${ formatContextSourceName ( selection . source ) } `
657868 : `Pattern repository · ${ selection . source } ` ,
658869 description : `${ matches . length } finding${ matches . length === 1 ? '' : 's' } across ${ reviewMap . size } review${ reviewMap . size === 1 ? '' : 's' } .` ,
659870 reviews : Array . from ( reviewMap . values ( ) ) ,
@@ -945,6 +1156,21 @@ export interface AnalyticsExportReport {
9451156 patternRepositoryBaselineLabeledTotal : number
9461157 patternRepositoryBaselineAcceptanceRate ?: number
9471158 patternRepositoryAcceptanceLift ?: number
1159+ contextSourceFindingTotal : number
1160+ contextSourceLabeledTotal : number
1161+ contextSourceAcceptedTotal : number
1162+ contextSourceRejectedTotal : number
1163+ contextSourceResolvedTotal : number
1164+ contextSourceReviewCount : number
1165+ contextSourceSourceCount : number
1166+ contextSourceUtilizationRate : number
1167+ contextSourceAcceptanceRate : number
1168+ contextSourceBaselineLabeledTotal : number
1169+ contextSourceBaselineAcceptanceRate ?: number
1170+ contextSourceAcceptanceLift ?: number
1171+ contextSourceFixRate : number
1172+ contextSourceBaselineFixRate ?: number
1173+ contextSourceFixLift ?: number
9481174 latestMicroF1 ?: number
9491175 latestWeightedScore ?: number
9501176 latestAcceptanceRate ?: number
@@ -954,6 +1180,8 @@ export interface AnalyticsExportReport {
9541180 feedbackLearningByReview : AnalyticsSnapshot [ 'feedbackLearningSeries' ]
9551181 patternRepositoryByReview : AnalyticsSnapshot [ 'patternRepositorySeries' ]
9561182 patternRepositorySources : AnalyticsSnapshot [ 'patternRepositorySourceData' ]
1183+ contextSourceByReview : AnalyticsSnapshot [ 'contextSourceSeries' ]
1184+ contextSources : AnalyticsSnapshot [ 'contextSourceData' ]
9571185 topAcceptedCategories : AnalyticsSnapshot [ 'topAcceptedCategories' ]
9581186 topRejectedCategories : AnalyticsSnapshot [ 'topRejectedCategories' ]
9591187 topAcceptedRules : AnalyticsSnapshot [ 'topAcceptedRules' ]
@@ -1024,6 +1252,22 @@ function appendPatternRepositoryRows(
10241252 } )
10251253}
10261254
1255+ function appendContextSourceRows (
1256+ rows : AnalyticsExportCsvRow [ ] ,
1257+ items : AnalyticsSnapshot [ 'contextSourceData' ] ,
1258+ ) {
1259+ items . forEach ( item => {
1260+ rows . push ( { report : 'reinforcement' , group : 'context_sources' , label : item . name , metric : 'total' , value : item . total } )
1261+ rows . push ( { report : 'reinforcement' , group : 'context_sources' , label : item . name , metric : 'labeled' , value : item . labeled } )
1262+ rows . push ( { report : 'reinforcement' , group : 'context_sources' , label : item . name , metric : 'accepted' , value : item . accepted } )
1263+ rows . push ( { report : 'reinforcement' , group : 'context_sources' , label : item . name , metric : 'rejected' , value : item . rejected } )
1264+ rows . push ( { report : 'reinforcement' , group : 'context_sources' , label : item . name , metric : 'resolved' , value : item . resolved } )
1265+ rows . push ( { report : 'reinforcement' , group : 'context_sources' , label : item . name , metric : 'review_count' , value : item . reviewCount } )
1266+ rows . push ( { report : 'reinforcement' , group : 'context_sources' , label : item . name , metric : 'acceptance_rate' , value : item . acceptanceRate } )
1267+ rows . push ( { report : 'reinforcement' , group : 'context_sources' , label : item . name , metric : 'fix_rate' , value : item . fixRate } )
1268+ } )
1269+ }
1270+
10271271export function buildAnalyticsExportReport (
10281272 reviews : ReviewSession [ ] ,
10291273 trends : AnalyticsTrendsResponse | undefined ,
@@ -1102,6 +1346,21 @@ export function buildAnalyticsExportReport(
11021346 patternRepositoryBaselineLabeledTotal : analytics . stats . patternRepositoryBaselineLabeledTotal ,
11031347 patternRepositoryBaselineAcceptanceRate : analytics . stats . patternRepositoryBaselineAcceptanceRate ?? undefined ,
11041348 patternRepositoryAcceptanceLift : analytics . stats . patternRepositoryAcceptanceLift ?? undefined ,
1349+ contextSourceFindingTotal : analytics . stats . contextSourceFindingTotal ,
1350+ contextSourceLabeledTotal : analytics . stats . contextSourceLabeledTotal ,
1351+ contextSourceAcceptedTotal : analytics . stats . contextSourceAcceptedTotal ,
1352+ contextSourceRejectedTotal : analytics . stats . contextSourceRejectedTotal ,
1353+ contextSourceResolvedTotal : analytics . stats . contextSourceResolvedTotal ,
1354+ contextSourceReviewCount : analytics . stats . contextSourceReviewCount ,
1355+ contextSourceSourceCount : analytics . stats . contextSourceSourceCount ,
1356+ contextSourceUtilizationRate : analytics . stats . contextSourceUtilizationRate ,
1357+ contextSourceAcceptanceRate : analytics . stats . contextSourceAcceptanceRate ,
1358+ contextSourceBaselineLabeledTotal : analytics . stats . contextSourceBaselineLabeledTotal ,
1359+ contextSourceBaselineAcceptanceRate : analytics . stats . contextSourceBaselineAcceptanceRate ?? undefined ,
1360+ contextSourceAcceptanceLift : analytics . stats . contextSourceAcceptanceLift ?? undefined ,
1361+ contextSourceFixRate : analytics . stats . contextSourceFixRate ,
1362+ contextSourceBaselineFixRate : analytics . stats . contextSourceBaselineFixRate ?? undefined ,
1363+ contextSourceFixLift : analytics . stats . contextSourceFixLift ?? undefined ,
11051364 latestMicroF1 : trendAnalytics . latestEval ?. micro_f1 ,
11061365 latestWeightedScore : trendAnalytics . latestEval ?. weighted_score ,
11071366 latestAcceptanceRate : trendAnalytics . latestFeedback ?. acceptance_rate ,
@@ -1111,6 +1370,8 @@ export function buildAnalyticsExportReport(
11111370 feedbackLearningByReview : analytics . feedbackLearningSeries ,
11121371 patternRepositoryByReview : analytics . patternRepositorySeries ,
11131372 patternRepositorySources : analytics . patternRepositorySourceData ,
1373+ contextSourceByReview : analytics . contextSourceSeries ,
1374+ contextSources : analytics . contextSourceData ,
11141375 topAcceptedCategories : analytics . topAcceptedCategories ,
11151376 topRejectedCategories : analytics . topRejectedCategories ,
11161377 topAcceptedRules : analytics . topAcceptedRules ,
@@ -1207,6 +1468,21 @@ export function buildAnalyticsCsv(report: AnalyticsExportReport): string {
12071468 }
12081469 } )
12091470 appendPatternRepositoryRows ( rows , report . reinforcement . patternRepositorySources )
1471+ report . reinforcement . contextSourceByReview . forEach ( point => {
1472+ rows . push ( { report : 'reinforcement' , group : 'context_source_by_review' , label : point . label , metric : 'findings' , value : point . findings } )
1473+ rows . push ( { report : 'reinforcement' , group : 'context_source_by_review' , label : point . label , metric : 'labeled' , value : point . labeled } )
1474+ rows . push ( { report : 'reinforcement' , group : 'context_source_by_review' , label : point . label , metric : 'accepted' , value : point . accepted } )
1475+ rows . push ( { report : 'reinforcement' , group : 'context_source_by_review' , label : point . label , metric : 'rejected' , value : point . rejected } )
1476+ rows . push ( { report : 'reinforcement' , group : 'context_source_by_review' , label : point . label , metric : 'resolved' , value : point . resolved } )
1477+ rows . push ( { report : 'reinforcement' , group : 'context_source_by_review' , label : point . label , metric : 'source_count' , value : point . sourceCount } )
1478+ if ( point . acceptanceRate != null ) {
1479+ rows . push ( { report : 'reinforcement' , group : 'context_source_by_review' , label : point . label , metric : 'acceptance_rate' , value : point . acceptanceRate } )
1480+ }
1481+ if ( point . fixRate != null ) {
1482+ rows . push ( { report : 'reinforcement' , group : 'context_source_by_review' , label : point . label , metric : 'fix_rate' , value : point . fixRate } )
1483+ }
1484+ } )
1485+ appendContextSourceRows ( rows , report . reinforcement . contextSources )
12101486 appendFeedbackBreakdownRows ( rows , 'top_accepted_categories' , report . reinforcement . topAcceptedCategories )
12111487 appendFeedbackBreakdownRows ( rows , 'top_rejected_categories' , report . reinforcement . topRejectedCategories )
12121488 appendFeedbackBreakdownRows ( rows , 'top_accepted_rules' , report . reinforcement . topAcceptedRules )
0 commit comments