Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2378,6 +2378,11 @@ <h3 class="text-xl font-semibold text-slate-900 dark:text-slate-100">${emptyTitl
<span class="truncate">Issues</span> ${getSortIcon('issues_count')}
</div>
</th>
<th class='cursor-pointer px-2 py-3 hover:bg-slate-100 dark:hover:bg-slate-900" data-sort-column="risk_summary" style="min-width: 200px;" title="AI-generated risk summmary for the PR">
<div class="flex items-center gap-1 min-w-0">
<span class="truncate">Risk Summary</span>
</div>
</th>
<th class="cursor-pointer px-2 py-3 hover:bg-slate-100 dark:hover:bg-slate-900" data-sort-column="last_updated_at" style="min-width: 90px;" title="Click to sort by PR last updated time. Shift+Click to add to sort columns.&#10;API: GET /repos/{owner}/{repo}/pulls/{pr_number} (updated_at field)">
<div class="flex items-center gap-1 min-w-0">
<i class="fas fa-clock text-slate-400 dark:text-slate-500 flex-shrink-0"></i>
Expand Down Expand Up @@ -2704,6 +2709,9 @@ <h3 class="text-xl font-semibold text-slate-900 dark:text-slate-100">${emptyTitl
<td class="px-2 py-3" id="readiness-issues-${pr.id}">
<span class="text-xs text-slate-400 dark:text-slate-500">-</span>
</td>
<td class="px-2 py-3 text-center" id="readiness-risk-summary-${pr.id}">
<span class="text-xs text-slate-400 dark:text-slate-500">-</span>
</td>
<td class="px-2 py-3 text-xs text-slate-600 dark:text-slate-400 truncate">
<span class="inline-flex items-center gap-1"><i class="fas fa-clock text-slate-400 dark:text-slate-500"></i>${escapeHtml(timeAgo(pr.last_updated_at))}</span>
</td>
Expand Down
85 changes: 79 additions & 6 deletions src/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ def parse_pr_url(pr_url):
"""
Parse GitHub PR URL to extract owner, repo, and PR number.

Security Hardening (Issue #45):
Security Hardening (Issue `#45`):
- Type validation to prevent type confusion attacks
- Anchored regex pattern to block malformed URLs with trailing junk
- Raises ValueError instead of returning None for better error handling
"""
# FIX Issue #45: Type validation
# FIX Issue `#45`: Type validation
if not isinstance(pr_url, str):
raise ValueError("PR URL must be a string")

Expand All @@ -30,12 +30,12 @@ def parse_pr_url(pr_url):

pr_url = pr_url.strip().rstrip('/')

# FIX Issue #45: Anchored regex - must match EXACTLY, no trailing junk allowed
# FIX Issue `#45`: Anchored regex - must match EXACTLY, no trailing junk allowed
pattern = r'^https?://github\.com/([^/]+)/([^/]+)/pull/(\d+)$'
match = re.match(pattern, pr_url)

if not match:
# FIX Issue #45: Raise error instead of returning None
# FIX Issue `#45`: Raise error instead of returning None
raise ValueError("Invalid GitHub PR URL. Format: https://github.com/OWNER/REPO/pull/NUMBER")

return {
Expand Down Expand Up @@ -576,7 +576,7 @@ def calculate_pr_readiness(pr_data, review_classification, review_score):
classification = 'NEEDS_WORK'
else:
classification = 'NOT_READY'

return {
'overall_score': overall_score,
'ci_score': ci_score,
Expand All @@ -585,5 +585,78 @@ def calculate_pr_readiness(pr_data, review_classification, review_score):
'merge_ready': merge_ready,
'blockers': blockers,
'warnings': warnings,
'recommendations': recommendations
'recommendations': recommendations,
'risk_summary': generate_ai_risk_summary({
'overall_score': overall_score,
'ci_score': ci_score,
'review_score': review_score,
'classification': classification,
'merge_ready': merge_ready,
'blockers': blockers,
'warnings': warnings,
'recommendations': recommendations
})
}


#Add ai summary feat
def generate_ai_risk_summary(pr_readiness_data):
"""
Generate a concise AI-powered risk summary for a PR.

Args:
pr_readiness_data: Dict from calculate_pr_readiness()

Returns:
str: AI-generated summary or fallback summary
"""
# Extract key data for the prompt
blockers = pr_readiness_data.get('blockers', [])
warnings = pr_readiness_data.get('warnings', [])
recommendations = pr_readiness_data.get('recommendations', [])
overall_score = pr_readiness_data.get('overall_score', 0)
classification = pr_readiness_data.get('classification', 'NOT_READY')

# Build the prompt for Gemini
prompt = (
f"Generate a concise, professional risk summary for a PR with the following details:\n"
f"- Classification: {classification}\n"
f"- Overall score: {overall_score}\n"
f"- Blockers: {blockers}\n"
f"- Warnings: {warnings}\n"
f"- Recommendations: {recommendations}\n\n"
f"Focus on why the PR may be risky and what should be addressed first. "
f"Keep the summary to 1-2 sentences. If the PR is ready, say so clearly."
)

# Call Gemini (pseudo-code; replace with actual API call)
try:
ai_summary = call_gemini_api(prompt)
return ai_summary
except Exception as e:
# Fallback to deterministic summary
return generate_fallback_summary(pr_readiness_data)


def generate_fallback_summary(pr_readiness_data):
"""
Generate a deterministic fallback summary if AI fails.
"""
blockers = pr_readiness_data.get('blockers', [])
warnings = pr_readiness_data.get('warnings', [])
classification = pr_readiness_data.get('classification', 'NOT_READY')

if blockers:
return (
f"This PR is not merge-ready due to {len(blockers)} blocker(s), "
f"including: {', '.join(blockers[:2])}. "
f"Address these issues before proceeding."
)
elif warnings:
return (
f"This PR is nearly ready but has {len(warnings)} warning(s), "
f"such as: {', '.join(warnings[:2])}. "
f"Review these before merging."
)
else:
return f"This PR is {classification.lower().replace('_', ' ')} and ready for review/merge."
Loading