1414import * as fs from 'fs' ;
1515import * as path from 'path' ;
1616import { IssueGroup } from '../utils/issue-grouping' ;
17+ import { createClient , SupabaseClient } from '@supabase/supabase-js' ;
18+ import { AppScoreManager } from './v9-app-score-manager' ;
19+ import { SkillScoreManager } from './v9-skill-score-manager' ;
1720
1821// ================================================================
1922// Types for Grouped Report
@@ -27,7 +30,8 @@ export interface EnrichedIssue {
2730 tool : string ;
2831 severity : 'critical' | 'high' | 'medium' | 'low' ;
2932 message : string ;
30- category : string ;
33+ category : string ; // Issue type: NEW, EXISTING_MODIFIED, RESOLVED, EXISTING_REST
34+ detectedCategory ?: string ; // Issue category: Security, Performance, Architecture, Dependencies, Code Quality
3135 snippet ?: string ;
3236 fixSuggestion ?: {
3337 fix : string ;
@@ -185,6 +189,28 @@ export interface FixLocation {
185189// ================================================================
186190
187191export class V9GroupedReportFormatter {
192+ private supabase : SupabaseClient | null = null ;
193+ private appScoreManager : AppScoreManager | null = null ;
194+ private skillScoreManager : SkillScoreManager | null = null ;
195+
196+ constructor ( ) {
197+ // Initialize Supabase if credentials are available
198+ try {
199+ const supabaseUrl = process . env . SUPABASE_URL ;
200+ const supabaseKey = process . env . SUPABASE_SERVICE_ROLE_KEY ;
201+
202+ if ( supabaseUrl && supabaseKey ) {
203+ this . supabase = createClient ( supabaseUrl , supabaseKey ) ;
204+ this . appScoreManager = new AppScoreManager ( this . supabase ) ;
205+ this . skillScoreManager = new SkillScoreManager ( this . supabase ) ;
206+ console . log ( '[V9GroupedReportFormatter] Supabase scoring managers initialized' ) ;
207+ } else {
208+ console . warn ( '[V9GroupedReportFormatter] Supabase credentials not found - using simplified scoring' ) ;
209+ }
210+ } catch ( error ) {
211+ console . error ( '[V9GroupedReportFormatter] Failed to initialize Supabase:' , error ) ;
212+ }
213+ }
188214
189215 /**
190216 * Generate grouped report with attachments
@@ -239,7 +265,7 @@ export class V9GroupedReportFormatter {
239265 markdown . push ( '' ) ;
240266
241267 // Executive Summary
242- markdown . push ( this . generateExecutiveSummary ( issues , groups , metadata ) ) ;
268+ markdown . push ( await this . generateExecutiveSummary ( issues , groups , metadata ) ) ;
243269 markdown . push ( '' ) ;
244270
245271 // Issue Groups by Severity (CRITICAL FIRST, then HIGH)
@@ -499,28 +525,170 @@ export class V9GroupedReportFormatter {
499525 }
500526
501527 /**
502- * Calculate quality score (0-100) based on issues
528+ * Calculate quality score (0-100) with full category breakdown
503529 *
504- * SCORING LOGIC (unified weights):
505- * - ALL existing issues (NEW/EXISTING_MODIFIED/EXISTING_REST): Same deductions
506- * - Critical: -5 points
507- * - High: -3 points
508- * - Medium: -1 point
509- * - Low: -0.5 points
510- *
511- * - RESOLVED issues: Same points as bonus (just flip the sign)
512- * - Critical: +5 points
513- * - High: +3 points
514- * - Medium: +1 point
515- * - Low: +0.5 points
516- *
517- * NOTE: This is simplified scoring for grouped reports.
518- * Full V9 pipeline uses per-category scoring (Security, Performance, Architecture, Dependency, Quality):
530+ * Uses V9 scoring system with per-category scores:
519531 * - APP score: Overall = MIN(category scores) - "weakest link" principle
520532 * - Skill score: Overall = AVERAGE(category scores)
521- * - See v9-app-score-manager.ts and v9-skill-score-manager.ts for full implementation
533+ * - Category scores: Security, Performance, Architecture, Dependency, Code Quality
534+ * - Baseline tracking via Supabase
535+ *
536+ * Falls back to simplified scoring if Supabase is not available.
537+ */
538+ private async calculateQualityScore (
539+ issues : EnrichedIssue [ ] ,
540+ metadata : { repository ?: string ; prAuthor ?: string ; prAuthorEmail ?: string }
541+ ) : Promise < {
542+ score : number ;
543+ grade : string ;
544+ breakdown : any ;
545+ categoryScores ?: {
546+ security : number ;
547+ performance : number ;
548+ architecture : number ;
549+ dependency : number ;
550+ codeQuality : number ;
551+ } ;
552+ appScore ?: number ;
553+ skillScore ?: number ;
554+ } > {
555+ // Use full V9 scoring if Supabase is available
556+ if ( this . appScoreManager && this . skillScoreManager && metadata . repository ) {
557+ return await this . calculateFullV9Score ( issues , metadata ) ;
558+ }
559+
560+ // Fall back to simplified scoring
561+ return this . calculateSimplifiedScore ( issues ) ;
562+ }
563+
564+ /**
565+ * Full V9 category-based scoring with Supabase persistence
522566 */
523- private calculateQualityScore ( issues : EnrichedIssue [ ] ) : { score : number ; grade : string ; breakdown : any } {
567+ private async calculateFullV9Score (
568+ issues : EnrichedIssue [ ] ,
569+ metadata : { repository ?: string ; prAuthor ?: string ; prAuthorEmail ?: string }
570+ ) : Promise < any > {
571+ try {
572+ // Separate issues by type
573+ const newIssues = issues . filter ( i => i . category === 'NEW' ) ;
574+ const existingModified = issues . filter ( i => i . category === 'EXISTING_MODIFIED' ) ;
575+ const existingRest = issues . filter ( i => i . category === 'EXISTING_REST' ) ;
576+ const resolvedIssues = issues . filter ( i => i . category === 'RESOLVED' ) ;
577+
578+ // Group issues by detected category (Security, Performance, etc.)
579+ const issuesByCategory = {
580+ security : issues . filter ( i => i . detectedCategory === 'Security' ) ,
581+ performance : issues . filter ( i => i . detectedCategory === 'Performance' ) ,
582+ architecture : issues . filter ( i => i . detectedCategory === 'Architecture' ) ,
583+ dependency : issues . filter ( i => i . detectedCategory === 'Dependencies' ) ,
584+ codeQuality : issues . filter ( i => i . detectedCategory === 'Code Quality' )
585+ } ;
586+
587+ // Calculate per-category scores
588+ const categoryScores = {
589+ security : this . calculateCategoryScore ( issuesByCategory . security ) ,
590+ performance : this . calculateCategoryScore ( issuesByCategory . performance ) ,
591+ architecture : this . calculateCategoryScore ( issuesByCategory . architecture ) ,
592+ dependency : this . calculateCategoryScore ( issuesByCategory . dependency ) ,
593+ codeQuality : this . calculateCategoryScore ( issuesByCategory . codeQuality )
594+ } ;
595+
596+ // Calculate APP score (minimum of categories - weakest link)
597+ const appScore = Math . min (
598+ categoryScores . security ,
599+ categoryScores . performance ,
600+ categoryScores . architecture ,
601+ categoryScores . dependency ,
602+ categoryScores . codeQuality
603+ ) ;
604+
605+ // Calculate Skill score (average of categories)
606+ const skillScore = Math . round (
607+ ( categoryScores . security +
608+ categoryScores . performance +
609+ categoryScores . architecture +
610+ categoryScores . dependency +
611+ categoryScores . codeQuality ) / 5
612+ ) ;
613+
614+ // Save to Supabase
615+ if ( this . appScoreManager && metadata . repository ) {
616+ await this . appScoreManager ! . saveAppScore ( {
617+ repository : metadata . repository ,
618+ prNumber : 0 , // Will be updated by caller
619+ appOverallScore : appScore ,
620+ appCategoryScores : categoryScores ,
621+ decision : appScore >= 70 ? 'APPROVED' : 'DECLINED' ,
622+ qualityScore : appScore ,
623+ issueCounts : {
624+ new : newIssues . length ,
625+ existing : existingModified . length + existingRest . length ,
626+ resolved : resolvedIssues . length ,
627+ blocking : newIssues . filter ( i => i . severity === 'critical' || i . severity === 'high' ) . length
628+ }
629+ } ) ;
630+ }
631+
632+ if ( this . skillScoreManager && metadata . prAuthorEmail && metadata . repository ) {
633+ await this . skillScoreManager ! . saveSkillScore ( {
634+ developerEmail : metadata . prAuthorEmail ,
635+ developerName : metadata . prAuthor ,
636+ repository : metadata . repository ,
637+ prNumber : 0 , // Will be updated by caller
638+ overallScore : skillScore ,
639+ categoryScores : categoryScores ,
640+ issueCounts : {
641+ new : newIssues . length ,
642+ resolved : resolvedIssues . length ,
643+ critical : issues . filter ( i => i . severity === 'critical' ) . length ,
644+ high : issues . filter ( i => i . severity === 'high' ) . length ,
645+ medium : issues . filter ( i => i . severity === 'medium' ) . length ,
646+ low : issues . filter ( i => i . severity === 'low' ) . length
647+ }
648+ } ) ;
649+ }
650+
651+ // Determine grade based on appScore
652+ let grade : string ;
653+ if ( appScore >= 90 ) grade = 'A' ;
654+ else if ( appScore >= 80 ) grade = 'B' ;
655+ else if ( appScore >= 70 ) grade = 'C' ;
656+ else if ( appScore >= 60 ) grade = 'D' ;
657+ else grade = 'F' ;
658+
659+ return {
660+ score : appScore ,
661+ grade,
662+ categoryScores,
663+ appScore,
664+ skillScore,
665+ breakdown : {
666+ baseScore : 100 ,
667+ categoryScores,
668+ overallMethod : 'MIN (weakest link)' ,
669+ skillScoreMethod : 'AVERAGE'
670+ }
671+ } ;
672+ } catch ( error ) {
673+ console . error ( '[V9GroupedReportFormatter] Error calculating full V9 score:' , error ) ;
674+ // Fall back to simplified scoring
675+ return this . calculateSimplifiedScore ( issues ) ;
676+ }
677+ }
678+
679+ /**
680+ * Calculate score for a single category
681+ */
682+ private calculateCategoryScore ( categoryIssues : EnrichedIssue [ ] ) : number {
683+ const baseScore = 100 ;
684+ const deduction = this . calculateImpact ( categoryIssues ) ;
685+ return Math . max ( 0 , Math . min ( 100 , baseScore - deduction ) ) ;
686+ }
687+
688+ /**
689+ * Simplified scoring (fallback when Supabase unavailable)
690+ */
691+ private calculateSimplifiedScore ( issues : EnrichedIssue [ ] ) : any {
524692 const baseScore = 100.0 ;
525693
526694 // Separate issues by category
@@ -531,8 +699,8 @@ export class V9GroupedReportFormatter {
531699
532700 // Calculate deductions - SAME weights for ALL issue categories
533701 const newIssuesDeduction = this . calculateImpact ( newIssues ) ;
534- const existingModifiedDeduction = this . calculateImpact ( existingModified ) ; // Same weight
535- const existingRestDeduction = this . calculateImpact ( existingRest ) ; // Same weight
702+ const existingModifiedDeduction = this . calculateImpact ( existingModified ) ;
703+ const existingRestDeduction = this . calculateImpact ( existingRest ) ;
536704
537705 // Calculate bonuses - SAME weights (just positive instead of negative)
538706 const resolutionBonus = this . calculateImpact ( resolvedIssues ) ;
@@ -606,11 +774,11 @@ export class V9GroupedReportFormatter {
606774 /**
607775 * Generate executive summary
608776 */
609- private generateExecutiveSummary (
777+ private async generateExecutiveSummary (
610778 issues : EnrichedIssue [ ] ,
611779 groups : IssueGroup [ ] ,
612780 metadata : any
613- ) : string {
781+ ) : Promise < string > {
614782 const bySeverity = this . groupBySeverity ( issues ) ;
615783 const byCategory = this . groupByCategory ( issues ) ;
616784
@@ -620,8 +788,12 @@ export class V9GroupedReportFormatter {
620788 ( i . severity === 'critical' || i . severity === 'high' )
621789 ) ;
622790
623- // Calculate quality score
624- const qualityResult = this . calculateQualityScore ( issues ) ;
791+ // Calculate quality score with full V9 scoring
792+ const qualityResult = await this . calculateQualityScore ( issues , {
793+ repository : metadata . repository ,
794+ prAuthor : metadata . prAuthor ,
795+ prAuthorEmail : metadata . prAuthorEmail
796+ } ) ;
625797 const scoreInterpretation = this . getScoreInterpretation ( qualityResult . score ) ;
626798
627799 // Calculate auto-fixable coverage
@@ -640,13 +812,28 @@ ${scoreInterpretation.emoji} **${qualityResult.score.toFixed(1)}/100** (Grade: *
640812> ${ scoreInterpretation . description }
641813
642814**Score Breakdown**:
815+ ${ qualityResult . categoryScores ? `
816+ **Category Scores** (Repository Health):
817+ - 🔒 Security: ${ qualityResult . categoryScores . security } /100
818+ - ⚡ Performance: ${ qualityResult . categoryScores . performance } /100
819+ - 🏗️ Architecture: ${ qualityResult . categoryScores . architecture } /100
820+ - 📦 Dependencies: ${ qualityResult . categoryScores . dependency } /100
821+ - ✨ Code Quality: ${ qualityResult . categoryScores . codeQuality } /100
822+
823+ **Overall Scores**:
824+ - 📱 **APP Score**: ${ qualityResult . appScore } /100 (MIN of categories - "weakest link")
825+ - 👨💻 **Skill Score**: ${ qualityResult . skillScore } /100 (AVERAGE of categories)
826+
827+ > Scores saved to Supabase for tracking trends over time
828+ ` : `
643829- Base Score: 100.0
644- - NEW issues: -${ qualityResult . breakdown . newIssuesDeduction . toFixed ( 1 ) }
645- - EXISTING_MODIFIED issues: -${ qualityResult . breakdown . existingModifiedDeduction . toFixed ( 1 ) }
646- - EXISTING_REST issues: -${ qualityResult . breakdown . existingRestDeduction . toFixed ( 1 ) } ${ qualityResult . breakdown . resolutionBonus > 0 ? `
830+ - NEW issues: -${ qualityResult . breakdown . newIssuesDeduction ? .toFixed ( 1 ) || '0.0' }
831+ - EXISTING_MODIFIED issues: -${ qualityResult . breakdown . existingModifiedDeduction ? .toFixed ( 1 ) || '0.0' }
832+ - EXISTING_REST issues: -${ qualityResult . breakdown . existingRestDeduction ? .toFixed ( 1 ) || '0.0' } ${ qualityResult . breakdown . resolutionBonus > 0 ? `
647833- RESOLVED issues: +${ qualityResult . breakdown . resolutionBonus . toFixed ( 1 ) } ` : '' }
648834
649835> All issue categories use the same scoring: Critical=-5, High=-3, Medium=-1, Low=-0.5
836+ ` }
650837
651838---
652839
0 commit comments