Skip to content

Commit 52ec1d4

Browse files
push
1 parent eb701be commit 52ec1d4

7 files changed

Lines changed: 65 additions & 54 deletions

File tree

cli/main.py

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -596,6 +596,9 @@ def batch_analyze(
596596
overwrite: bool = typer.Option(
597597
False, "--overwrite", "--full-sync", help="Ignore existing CSV; fetch full date range (default: incremental fetch from latest CSV data)"
598598
),
599+
fetch_only: bool = typer.Option(
600+
False, "--fetch-only", help="Only fetch PR URLs to cache; skip analysis and labeling"
601+
),
599602
):
600603
"""
601604
Batch analyze multiple PRs from a file or date range.
@@ -647,26 +650,26 @@ def batch_analyze(
647650
)
648651
raise typer.Exit(1)
649652

650-
# Require output_file unless --label is used
651-
if not label and not output_file:
652-
typer.echo("Error: --output is required unless --label is used", err=True)
653+
# Require output_file unless --label or --fetch-only is used
654+
if not label and not output_file and not fetch_only:
655+
typer.echo("Error: --output is required unless --label or --fetch-only is used", err=True)
653656
raise typer.Exit(1)
654657

655658
# When labeling, default to writing CSV as well (override with --output)
656659
if label and not output_file:
657660
output_file = Path("complexity-report.csv")
658661

659-
# Get credentials
660-
openai_key = get_openai_api_key()
662+
# Get credentials (skip LLM keys when fetch-only)
663+
openai_key = get_openai_api_key() if not fetch_only else None
661664

662-
if provider == "openai" and not openai_key:
665+
if not fetch_only and provider == "openai" and not openai_key:
663666
typer.echo("Error: OPENAI_API_KEY environment variable is required for openai provider", err=True)
664667
typer.echo("Set it with: export OPENAI_API_KEY='your-key'", err=True)
665668
raise typer.Exit(1)
666-
if provider == "anthropic" and not get_anthropic_api_key():
669+
if not fetch_only and provider == "anthropic" and not get_anthropic_api_key():
667670
typer.echo("Error: ANTHROPIC_API_KEY is required for anthropic provider", err=True)
668671
raise typer.Exit(1)
669-
if provider == "bedrock":
672+
if not fetch_only and provider == "bedrock":
670673
typer.echo("Using Bedrock provider. Ensure AWS_PROFILE and AWS_REGION are set.", err=True)
671674

672675
# Get GitHub tokens - CLI option takes precedence over environment
@@ -711,12 +714,15 @@ def batch_analyze(
711714
err=True,
712715
)
713716

714-
# Load prompt
715-
try:
716-
prompt_text = load_prompt(prompt_file)
717-
except FileNotFoundError as e:
718-
typer.echo(f"Error: {e}", err=True)
719-
raise typer.Exit(1)
717+
# Load prompt (skip when fetch-only)
718+
if not fetch_only:
719+
try:
720+
prompt_text = load_prompt(prompt_file)
721+
except FileNotFoundError as e:
722+
typer.echo(f"Error: {e}", err=True)
723+
raise typer.Exit(1)
724+
else:
725+
prompt_text = None
720726

721727
# Get PR URLs
722728
if input_file:
@@ -785,6 +791,14 @@ def batch_analyze(
785791
since_override=since_override,
786792
)
787793

794+
# Fetch-only mode: save to cache and exit
795+
if fetch_only:
796+
if not cache_file:
797+
typer.echo("Error: --cache is required with --fetch-only", err=True)
798+
raise typer.Exit(1)
799+
typer.echo(f"✓ Fetched {len(pr_urls)} PR URLs to cache: {cache_file}", err=True)
800+
return
801+
788802
# Create analyzer function with progress callback
789803
def progress_msg(msg: str) -> None:
790804
"""Display progress messages."""

reports/advanced/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
"""Advanced reports (13, 15, 16)."""
1+
"""Advanced reports (15, 16)."""
22

33
from .reports import (
44
report_complexity_trend_by_team,
5-
report_complexity_weighted_velocity,
65
report_cumulative_complexity,
76
)

reports/advanced/reports.py

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -19,36 +19,6 @@ def _ensure_date(df: pd.DataFrame) -> pd.DataFrame:
1919
return df
2020

2121

22-
def report_complexity_weighted_velocity(df: pd.DataFrame, output_dir: Path) -> Optional[str]:
23-
"""Report 13: Complexity Weighted Velocity - per sprint, total_complexity / #developers."""
24-
df = _ensure_date(df)
25-
if df.empty:
26-
return None
27-
df = df.copy()
28-
df["sprint"] = pd.to_datetime(df["date"]).dt.to_period("2W").dt.start_time
29-
sprint_total = df.groupby("sprint")["complexity"].sum()
30-
dev_col = "developer" if "developer" in df.columns else "author"
31-
sprint_devs = df.groupby("sprint")[dev_col].nunique().replace(0, 1)
32-
velocity = (sprint_total / sprint_devs).sort_index()
33-
if not has_plottable_series(velocity):
34-
return None
35-
fig, ax = plt.subplots(figsize=(12, 6))
36-
velocity.index = pd.to_datetime(velocity.index).strftime("%Y-%m-%d")
37-
velocity.plot(kind="bar", ax=ax, color="green", alpha=0.8)
38-
ax.set_title(
39-
"Complexity Weighted Velocity (per Sprint, per Developer)\n"
40-
"What: Output per sprint normalized by headcount. When: Sprint reviews. How: Compare bars for velocity trends."
41-
)
42-
ax.set_ylabel("Complexity / #developers")
43-
ax.set_xlabel("Sprint")
44-
ax.tick_params(axis="x", rotation=45)
45-
fig.tight_layout()
46-
out = output_dir / "13-complexity-weighted-velocity.png"
47-
fig.savefig(out, dpi=150, bbox_inches="tight")
48-
plt.close(fig)
49-
return str(out) if validate_png_has_content(out) else None
50-
51-
5222
def report_complexity_trend_by_team(df: pd.DataFrame, output_dir: Path) -> Optional[str]:
5323
"""Report 15: Complexity Trend by Team - rolling median."""
5424
df = _ensure_date(df)

reports/runner.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,6 @@ def run_reports(
8282
from reports.risk import report_complexity_histogram
8383
from reports.fairness import report_pr_size_vs_complexity
8484
from reports.fairness import report_pr_count_vs_avg_complexity
85-
from reports.advanced import report_complexity_weighted_velocity
8685
from reports.advanced import report_complexity_trend_by_team
8786
from reports.advanced import report_cumulative_complexity
8887

@@ -104,7 +103,6 @@ def run_reports(
104103
(report_complexity_histogram, "risk"),
105104
(report_pr_size_vs_complexity, "fairness"),
106105
(report_pr_count_vs_avg_complexity, "fairness"),
107-
(report_complexity_weighted_velocity, "advanced"),
108106
(report_complexity_trend_by_team, "advanced"),
109107
(report_cumulative_complexity, "advanced"),
110108
]

reports/team/reports.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ def report_complexity_distribution_by_team(df: pd.DataFrame, output_dir: Path) -
6363

6464

6565
def report_developer_contribution(df: pd.DataFrame, output_dir: Path) -> Optional[Union[str, List[str]]]:
66-
"""Report 5: Developer Complexity Contribution - stacked by sprint, one per team."""
66+
"""Report 5: Developer Complexity Contribution - stacked by week, one per team."""
6767
mapping = load_team_mapping()
6868
if not mapping:
6969
return None
@@ -78,12 +78,12 @@ def report_developer_contribution(df: pd.DataFrame, output_dir: Path) -> Optiona
7878
df = df[df["developer"] != ""]
7979
if df.empty:
8080
return None
81-
df["sprint"] = pd.to_datetime(df["date"]).dt.to_period("2W").dt.start_time
81+
df["week"] = pd.to_datetime(df["date"]).dt.to_period("W").dt.start_time
8282
generated = []
8383
for team in df["team"].unique():
8484
tdf = df[df["team"] == team]
8585
pivot = tdf.pivot_table(
86-
index="sprint", columns="developer", values="complexity", aggfunc="sum", fill_value=0
86+
index="week", columns="developer", values="complexity", aggfunc="sum", fill_value=0
8787
)
8888
pivot = pivot.reindex(pivot.sum().sort_values(ascending=False).index, axis=1)
8989
if not has_plottable_agg(pivot):
@@ -92,11 +92,11 @@ def report_developer_contribution(df: pd.DataFrame, output_dir: Path) -> Optiona
9292
fig, ax = plt.subplots(figsize=(12, 6))
9393
pivot.plot(kind="bar", stacked=True, ax=ax, width=0.8, legend=True)
9494
ax.set_title(
95-
f"Developer Complexity Contribution — {team} (per Sprint)\n"
96-
"What: Who delivered what per sprint. When: Sprint reviews. How: Compare stacked bars."
95+
f"Developer Complexity Contribution — {team} (per Week)\n"
96+
"What: Who delivered what per week. When: Weekly reviews. How: Compare stacked bars."
9797
)
9898
ax.set_ylabel("Complexity")
99-
ax.set_xlabel("Sprint")
99+
ax.set_xlabel("Week")
100100
ax.tick_params(axis="x", rotation=45)
101101
ax.legend(bbox_to_anchor=(1.02, 1), ncol=2)
102102
fig.tight_layout()

scripts/fetch-jan-feb-2026.sh

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#!/bin/bash
2+
# Fetch January–February 2026 PR URLs to cache (no analysis or labeling).
3+
# Run with --fetch-only first, then later run without it to analyze and label.
4+
5+
cd "$(dirname "$0")/.."
6+
7+
complexity-cli batch-analyze \
8+
--all-repos \
9+
--since 2026-01-01 \
10+
--until 2026-02-28 \
11+
--overwrite \
12+
--fetch-only \
13+
--cache cache/jan-feb-2026-prs.txt
14+
15+
echo "PR URLs cached to: cache/jan-feb-2026-prs.txt"

scripts/fetch-oct-dec-2025.sh

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#!/bin/bash
2+
# Fetch October–December 2025 PR URLs to cache (no analysis or labeling).
3+
# Run with --fetch-only first, then later run without it to analyze and label.
4+
5+
cd "$(dirname "$0")/.."
6+
7+
complexity-cli batch-analyze \
8+
--all-repos \
9+
--since 2025-10-01 \
10+
--until 2025-12-31 \
11+
--overwrite \
12+
--fetch-only \
13+
--cache cache/oct-dec-2025-prs.txt
14+
15+
echo "PR URLs cached to: cache/oct-dec-2025-prs.txt"

0 commit comments

Comments
 (0)