
    iI              
          U d Z ddlZddlZddlZddlZddlZddlmZ ddlm	Z	 ddddddddZ
eeef   ed	<   d
ZdZej"                  j%                  d e ee      j)                         j*                  j*                              Z e ee      dz        Z e ee      dz        ZdedefdZdeeeee	f   f   defdZdedeee	f   dz  fdZdeee	f   deddfdZdedeee	f   fdZdedefdZdeee	f   deee	f   fdZ d0de!e   dedz  de"e#eef   fdZ$d edeee	f   fd!Z%d edeee	f   fd"Z&d edeee	f   fd#Z'd edeee	f   fd$Z(d edeee	f   fd%Z)d edeee	f   fd&Z*d edeee	f   fd'Z+d edeeeee	f   f   fd(Z,deeeee	f   f   d)eee	f   dz  d*eee	f   dz  deee	f   fd+Z-d0d,e!e   dz  dej\                  fd-Z/d0d,e!e   dz  de#fd.Z0e1d/k(  r ejd                   e0              yy)1zI
health_score.py - Project health score calculator
A2 + A13 + A17 bundle
    N)Path)Anyg?g333333?g?test_pass_ratepyright_errorscode_coverage	tech_debtsecuritydocumentationdeploy_stabilityWEIGHTS)fixu   수정u   버그g      >@WORKSPACE_ROOTzmemory/whisper/qa-baseline.jsonzmemory/task-timers.jsonscorereturnc                 4    | dk\  ry| dk\  ry| dk\  ry| dk\  ryy	)
z&Convert numeric score to letter grade.Z   AP   BF   C<   DF )r   s    +/home/jay/workspace/scripts/health_score.pyscore_to_grader   )   s-    {	"	"	"    
categoriesc                     d}t         j                         D ]/  \  }}| j                  |i       }||j                  dd      |z  z  }1 t        |d      S )z4Compute weighted average score from category scores.        r   r      )r   itemsgetround)r    totalkeyweightcats        r   calculate_weighted_scorer+   <   sX    E}} .VnnS"%!$v--. ?r   pathc                     	 t        | dd      5 }t        j                  |      cddd       S # 1 sw Y   yxY w# t        t        j                  f$ r Y yw xY w)z?Load baseline JSON. Returns None if file is missing or invalid.rutf-8encodingN)openjsonloadFileNotFoundErrorJSONDecodeError)r,   fs     r   load_baseliner8   J   sQ    $g. 	 !99Q<	  	  	 t334 s#   ; /	; 8; ; AAresultc                     t        |      j                  j                  dd       t        |dd      5 }t	        j
                  | |dd       d	d	d	       y	# 1 sw Y   y	xY w)
z%Save current result as baseline JSON.Tparentsexist_okwr/   r0   Fr#   ensure_asciiindentN)r   parentmkdirr2   r3   dump)r9   r,   r7   s      r   save_baselinerE   S   sT    JD48	dC'	* ;a		&!%:; ; ;s   AA!c                     	 t        | dd      5 }t        j                  |      }ddd       j                  di       S # 1 sw Y   xY w# t        t        j
                  f$ r i cY S w xY w)z>Load tasks from task-timers.json. Returns empty dict on error.r.   r/   r0   Ntasks)r2   r3   r4   r%   r5   r6   )r,   r7   datas      r   load_task_timersrI   _   sh    $g. 	0!#'99Q<D	0xx$$	0 	0 t334 	s'   A A A  A	A A*)A*descriptionc                 T    | j                         t        fdt        D              S )z=Return True if the task description contains any fix keyword.c              3   &   K   | ]  }|v  
 y wNr   ).0kwlowers     r   	<genexpr>z_is_fix_task.<locals>.<genexpr>q   s     2rrU{2s   )rP   anyFIX_KEYWORDS)rJ   rP   s    @r   _is_fix_taskrT   n   s"    E2\222r   rG   c                     | sdddddS t        |       }t        d | j                         D              }t        ||z  dz  d      }|t        kD  }||||dS )z
    Calculate fix task ratio from tasks dict.

    Returns:
        {"total": N, "fix_count": N, "pct": X.X, "warning": bool}
    r   r"   F)r'   	fix_countpctwarningc              3   X   K   | ]"  }t        |j                  d d            sd $ yw)rJ       N)rT   r%   )rN   ts     r   rQ   z$calculate_fix_pct.<locals>.<genexpr>   s#     X!aeeMSU>V1WAXs    **d   r[   )lensumvaluesr&   FIX_WARNING_THRESHOLD)rG   r'   rV   rW   rX   s        r   calculate_fix_pctrb   t   se     35IIJEXu||~XXI
	E!C'
+C))G37SSr   cmdcwdc                     	 t        j                  | dd|d      }|j                  |j                  |j                  fS # t
        t         j                  f$ r}ddt        |      fcY d}~S d}~ww xY w)z4Run subprocess, return (returncode, stdout, stderr).Tr   )capture_outputtextrd   timeoutrZ   N)
subprocessrun
returncodestdoutstderrr5   TimeoutExpiredstr)rc   rd   proces       r   _runrs      sj    
~~
 T[[88z889 2s1v~s   <? A0A+%A0+A0project_dirc                     t        g d|       \  }}}|dk(  rdd| dS d}d}||z   j                         D ]m  }|j                         }|j                         }t	        |      D ]=  \  }	}
|
dv r|	dkD  r	 t        ||	dz
           }|
d	v s&|	dkD  s,	 t        ||	dz
           }? o ||z   }|dk(  rdd
dS ||z  dz  }t        t        |            }|| d| d|dddS # t        $ r Y fw xY w# t        $ r Y w xY w)z/Run pytest and compute pass rate score (0-100).)python3-mpytest--tb=no-q--no-headerrd   ri   r   zpytest not available: r   details)passedzpassed,r[   )failedzfailed,z&No tests found or pytest failed to runr]   /z tests passed (.1f%))rs   
splitlinesrP   split	enumerateint
ValueErrorr&   )rt   rcrm   rn   r   r   line
line_lowerpartsipartr'   rW   r   s                 r   collect_test_pass_rater      s]   CB 
Rx)?x'HII FF&,,. ZZ\
  " ' 
	GAt,,Q q1u.F ,,Q q1u.F
		  VOEz'OPP
5.3
Cc
OE&5'S	QS'TUU " 
 " s$   6C"C1"	C.-C.1	C=<C=c                    t        dd| g      \  }}}|dk(  rdd| dS 	 t        j                  |      }|j                  di       j                  dd      }|dk(  rd
}n"|dk  rd}n|dk  rd}n|dk  rd}n
|dk  rd}nd}|| ddS # t        j                  t
        f$ r# t        d	 |j                         D              }Y mw xY w)z+Run pyright and score based on error count.pyrightz--outputjsonri   r   zpyright not available: r}   summary
errorCountc              3   F   K   | ]  }d |j                         v sd  yw)z error r[   NrP   )rN   r   s     r   rQ   z)collect_pyright_errors.<locals>.<genexpr>   s     YyDJJL?X!Ys   !!r]      r   
   r      (   2   z pyright error(s))rs   r3   loadsr%   r6   KeyErrorr_   r   )rt   r   rm   rn   rH   error_countr   s          r   collect_pyright_errorsr      s    y.+FGB	Rx)@'IJJZzz&!hhy"-11,B
 a								+6G'HII#   (+ ZY(9(9(;YYZs   7B
 
9CCc                    t        g d|       \  }}}|dk(  rdd| dS ||z   }|j                         D ]  }|j                         j                  d      s#|j	                         }t        |      D ]P  }|j                  d      s	 t        |j                  d            }t        t        |            }	|	d	|d
ddc c S   dddS # t        $ r Y ew xY w)z0Run pytest with coverage and extract coverage %.)rv   rw   rx   z--cov=.z--cov-report=term-missingry   rz   r{   r|   ri   r   zpytest-cov not available: r}   TOTAL%z
Coverage: r   zCoverage data not found)rs   r   strip
startswithr   reversedendswithfloatrstripr   r&   r   )
rt   r   rm   rn   combinedr   r   r   covr   s
             r   collect_code_coverager      s    kB 
Rx)CF8'LMMH##% 
::<""7+JJLE  ==%#DKK$45 #E#J).ZCyPQ;RSS
 #<== & s   
7C	CCc                 t   t        |       dz  dz  }t        t              dz  }d}d}||fD ]L  }|j                         s	 t        |dd      5 }t	        j
                  |      }ddd       t        |      } n |t        |t              r|n|j                  d	g       }t        |t              rt        d
 |D              }t        |      }	nd}d}	|	dk(  rd}
n|	|z
  |	z  dz  }t        t        |            }
|
d| d| dS t!        ddddd| g      \  }}}|dk(  rdddS t        |j#                         D cg c]  }|j%                         s| c}      }|dk(  rdddS |dk  rd| ddS |dk  rd| ddS d| ddS # 1 sw Y   %xY w# t        j                  t        f$ r Y w xY wc c}w )zICheck tech-debt.json if available, otherwise use radon/flake8 complexity.memoryztech-debt.jsonzmemory/tech-debt.jsonNrZ   r.   r/   r0   r$   c              3   n   K   | ]-  }t        |t              s|j                  d d      dk(  s*d / yw)statusr2   r[   N)
isinstancedictr%   )rN   items     r   rQ   z$collect_tech_debt.<locals>.<genexpr>  s.     t4JtT4JtxxX`bhOimsOsQts   555r   r]   zTech debt: z open items from r}   rv   rw   flake8z--max-complexity=10z
--select=Cri   r   z/Tech debt: no data available (flake8 not found)r   z No high-complexity code detectedr   r   z high-complexity function(s)      )r   _WORKSPACE_ROOTexistsr2   r3   r4   rp   r6   OSErrorr   listr%   r_   r^   r   r&   rs   r   r   )rt   	debt_pathworkspace_debt	debt_data	used_pathpr7   r$   
open_counttotal_countr   
closed_pctr   rm   _r   complexity_issuess                    r   collect_tech_debtr      s    [!H,/??I/*-DDN'+II( 88:!S73 -q $		!I-F	 '	48	immGUW>XeT"t5ttJe*KJK!E%
2kACGJj)*E[DUV_U`+abb )T85JLZefgMB	Rx(YZZf.?.?.ARdTZZ\TRSA(JKK	a	+<*==Y(Z[[	b	 +<*==Y(Z[[+<*==Y(Z[[K- - (('2 2 Ss6   FF%F;F5F5F	FF21F2c           	         t        dddddd| g      \  }}}|dk(  rdd	| d
S 	 t        j                  |      }|j                  dg       }t	        d |D              }t	        d |D              }t	        d |D              }|dk(  r|dk(  r|dk(  rd}	n|dk(  r|dk(  rd}	n|dk(  rd}	n
|dk  rd}	nd}	|	d| d| d| dd
S # t        j
                  t        f$ r ddd
cY S w xY w)zRun bandit security scan.rv   rw   banditz-rz-fr3   ri   r   zbandit not available: r}   resultsc              3   h   K   | ]*  }|j                  d d      j                         dk(  s'd , yw)issue_severityrZ   HIGHr[   Nr%   upperrN   r.   s     r   rQ   z#collect_security.<locals>.<genexpr>6  s-     W/?)D)J)J)LPV)V1W   (22c              3   h   K   | ]*  }|j                  d d      j                         dk(  s'd , yw)r   rZ   MEDIUMr[   Nr   r   s     r   rQ   z#collect_security.<locals>.<genexpr>7  s-     [11551A2+F+L+L+NRZ+ZQ[r   c              3   h   K   | ]*  }|j                  d d      j                         dk(  s'd , yw)r   rZ   LOWr[   Nr   r   s     r   rQ   z#collect_security.<locals>.<genexpr>8  s,     U.>(C(I(I(Ku(T!Ur   zbandit output parse errorr]   U   A   r#   r   r   z
Security: z high, z	 medium, z low issues)rs   r3   r   r%   r_   r6   r   )
rt   r   rm   rn   rH   r   highmediumlowr   s
             r   collect_securityr   +  s+   	D(D$DB 
Rx)?x'HIIDzz&!((9b)WgWW[[[UWUU qyVq[SAX	v{		D6	RUQVVa'bcc   (+ D'BCCDs   AC C#"C#c                    t        t        |       j                  d            }|sdddS ddl}d}d}|D ]  }	 |j	                  d      }|j                  |      }|j                  |      D ]L  }t        ||j                  |j                  |j                  f      s1|dz  }|j                  |      sH|dz  }N  |dk(  rdd	dS ||z  d
z  }	t        t        |	            }
|
d| d| d|	dddS # t        t        f$ r Y w xY w)zGEstimate documentation coverage by checking docstrings in Python files.z*.pyr   zNo Python files foundr}   Nr/   r0   r[   zNo functions/classes foundr]   zDocumentation: r   z functions/classes (r   r   )r   r   rglobast	read_textparseSyntaxErrorr   walkr   FunctionDefAsyncFunctionDefClassDefget_docstringr   r&   )rt   py_filesr   total_funcs
documentedpy_filesourcetreenoderW   r   s              r   collect_documentationr   J  s8   D%++F34H'>??KJ 
$	&&&8F99V$D HHTN 	$D$#2F2F UVq $$T*!OJ		$
$ a'CDD
{
"S
(Cc
OEAk]Rfgjknfooq'rss W% 		s   #C44DDc                     g d}t         fd|D              }t        g d       \  }}}|dk7  s|j                         s|rdddS d	d
dS |j                         j                         }t	        |      }t        d |D              }|dkD  r||z  dz  nd}	|	dk(  rd}
n|	dk  rd}
n|	dk  rd}
n
|	dk  rd	}
nd}
|
d| d| d|	dddS )zw
    Estimate deploy stability from CI logs or git history.
    Falls back to checking for common CI config files.
    )z.github/workflowsz.gitlab-ci.ymlJenkinsfilez.circleci/config.ymlzazure-pipelines.ymlc              3   V   K   | ]   }t              |z  j                          " y wrM   )r   r   )rN   cfrt   s     r   rQ   z+collect_deploy_stability.<locals>.<genexpr>u  s$     J2d;'",446Js   &))gitlogz	--onelinez-50z--format=%sr|   r   r   z+CI config present; no git history availabler}   r   z!No CI config or git history foundc              3   J   K   | ]  t        fd dD              rd  yw)c              3   B   K   | ]  }|j                         v   y wrM   r   )rN   rO   cs     r   rQ   z5collect_deploy_stability.<locals>.<genexpr>.<genexpr>  s     g2rQWWYgs   )reverthotfixrollback	emergencyzcritical fixr[   N)rR   )rN   r   s    @r   rQ   z+collect_deploy_stability.<locals>.<genexpr>  s(      g)fgg 	
s    #r]   r   r   r   r   r   zDeploy stability: r   z problematic commits (r   r   )rR   rs   r   r   r^   r_   )rt   ci_files
ci_presentr   rm   r   commitsr'   bad_commitsbad_pctr   s   `          r   collect_deploy_stabilityr   h  s   H JJJJ 9MB
 
Qwflln,YZZ(KLLlln'')GLE  K
 ,119kE!C'!G!|	A	B	B '}AeW<RSZ[^R__ab r   c           	          t        |       t        |       t        |       t        |       t	        |       t        |       t        |       dS )zCollect all 7 category scores.r   )r   r   r   r   r   r   r   )rt   s    r   collect_all_categoriesr     sF     1=0=.{;&{3$[1.{;4[A r   baselinefix_pct_resultc                    t        t        t        |                   }t        |      }i }i }||j	                  di       }| j                         D ]s  \  }}t        |      }	|r\||v rX||   j	                  d|j	                  dd            }
|j	                  dd      }||
z
  }|dkD  rd}n
|dk  rd}nd}||	d<   ||	d<   |	||<   u |||d	}|||d
<   |S )zv
    Assemble the final result dict.
    Adds baseline delta/direction for each category if baseline is provided.
    r    r   r   improveddegradedstabledelta	direction)r   grader    fix_pct)r   r&   r+   r   r%   r$   r   )r    r   r  r   r  output_categoriesbaseline_catsr(   rH   cat_out
prev_score
curr_scorer  r  r9   s                  r   build_resultr    s    .z:;<E5!E 46$&M \26%%' )	T"&t*SM1&s+//'19MNJ'1-J+Eqy&	&	$	$GG#,GK !(#)" 'F !*yMr   argvc                 H   t        j                  d      }|j                  ddd       |j                  dt        dt         d	       |j                  d
dd       |j                  dt        dt         d	       |j                  dd d       |j                  |       S )Nz0Project health score calculator (A2 + A13 + A17))rJ   z--project-dir.z9Project directory to analyze (default: current directory))defaulthelpz
--baselinez"Baseline JSON file path (default: )z--save-baseline
store_truezSave current result as baseline)actionr  z--task-timersz task-timers.json path (default: z--outputz'Output JSON file path (default: stdout))argparseArgumentParseradd_argumentDEFAULT_BASELINE_PATHDEFAULT_TASK_TIMERS_PATH
parse_args)r  parsers     r   r  r    s    $$FF H  
 %12G1HJ  
 .  
 (/0H/IK  
 6  
 T""r   c           	         t        |       }t        t        |j                        j	                               }t        |      }t        |j                        }t        |j                        }t        |      }t        |||      }|j                  rt        ||j                         t        j                  |dd      }|j                  rFt        |j                        }	|	j                   j#                  dd       |	j%                  |d       nt'        |       |j)                  d	      r4|d
   }
|d   }|d   }t'        d|
 d| d| dt*        j,                         y)N)r   r  Fr#   r?   Tr;   r/   r0   rX   rW   rV   r'   z[WARNING] Fix task ratio is z% (r   z) - exceeds 30% threshold)filer   )r  rp   r   rt   resolver   r8   r   rI   task_timersrb   r  rE   r3   dumpsoutputrB   rC   
write_textprintr%   sysrn   )r  argsrt   r    r   rG   r  r9   output_jsonout_pathrW   countr'   s                r   mainr,    sA   dDd4++,4467K (4J T]]+H T--.E&u-N *xWF fdmm, **V%BK{{$dT:K':k )$U#{+w'*3%s5'5'AZ[	

 r   __main__rM   )3__doc__r  r3   osrj   r'  pathlibr   typingr   r   r   rp   r   __annotations__rS   ra   environr%   __file__r!  rB   r   r  r  r   r+   r8   rE   rI   boolrT   rb   r   tupler   rs   r   r   r   r   r   r   r   r   r  	Namespacer  r,  __name__exitr   r   r   <module>r:     sg  
   	  
   c5j	  + **..!13tH~7M7M7O7V7V7]7]3^_D14UUV tO47PPQ % C &c4S>.A)B u  S#X 5 ;$sCx. ; ; ;3 4S> 3c 3d 3TT#s(^ TS#X T,d3i cDj E#sC-4H "V "VS#X "VJJ JS#X J>>s >tCH~ >41\3 1\4S> 1\hd# d$sCx. d>ts ttCH~ t<2# 2$sCx. 2j
 
S$sCx.5H0I 
$+S$sCx.()+38nt#+ cNT)+ 
#s(^	+f#T#Y% #1C1C #@)tCy4 )3 )X zCHHTV r   