Function bodies 688 total
_opus_escalate function · python · L589-L674 (86 LOC)src/pinwheel/ai/interpreter.py
async def _opus_escalate(
raw_text: str,
first_pass: ProposalInterpretation,
system: str,
api_key: str,
season_id: str = "",
round_number: int | None = None,
db_session: object | None = None,
) -> ProposalInterpretation | None:
"""Escalate an uncertain interpretation to Opus for deeper analysis.
Called when Sonnet returns low confidence or clarification_needed.
Opus sees the original proposal plus Sonnet's analysis and produces
a refined interpretation. Returns None if Opus also fails.
"""
from pinwheel.ai.usage import (
cacheable_system,
extract_usage,
record_ai_usage,
track_latency,
)
opus_model = "claude-sonnet-4-6"
effects_summary = "; ".join(
f"{e.effect_type}: {e.description}" for e in first_pass.effects
) or "No effects identified"
clarification_note = ""
if first_pass.clarification_needed:
clarification_note = (
"\nThe interpreter flagged tinterpret_proposal_v2 function · python · L677-L846 (170 LOC)src/pinwheel/ai/interpreter.py
async def interpret_proposal_v2(
raw_text: str,
ruleset: RuleSet,
api_key: str,
amendment_context: str | None = None,
season_id: str = "",
round_number: int | None = None,
db_session: object | None = None,
) -> ProposalInterpretation:
"""Use Claude to interpret a proposal into structured effects (v2).
Two-tier interpretation: Sonnet interprets first (fast, cheap). If Sonnet
is uncertain (clarification_needed or confidence < 0.5), Opus gets a
second look with Sonnet's analysis. The player only sees the final result.
"""
from pinwheel.ai.usage import (
cacheable_system,
extract_usage,
record_ai_usage,
track_latency,
)
params_desc = _build_parameter_description(ruleset)
system = INTERPRETER_V2_SYSTEM_PROMPT.format(parameters=params_desc)
user_msg = f"Proposal: {raw_text}"
if amendment_context:
user_msg = f"Original proposal: {amendment_context}\n\nAmendment: {raw_text}"
_split_compound_clauses function · python · L849-L864 (16 LOC)src/pinwheel/ai/interpreter.py
def _split_compound_clauses(raw_text: str) -> list[str]:
"""Split a compound proposal into individual clauses.
Detects "and" or "," between parameter-change clauses. Only splits
when multiple independent parameter changes are present.
Returns a list of clause strings. Single proposals return a one-element list.
"""
import re
text = raw_text.strip()
# Split on " and " or ", " but only between clauses (not inside phrases like
# "three point" — we require a number on each side of the split)
# Strategy: split on " and " or commas, then test each clause independently
separators = re.split(r"\s+and\s+|,\s*", text, flags=re.IGNORECASE)
# Filter out empty strings
return [s.strip() for s in separators if s.strip()]interpret_strategy_mock function · python · L1215-L1271 (57 LOC)src/pinwheel/ai/interpreter.py
def interpret_strategy_mock(raw_text: str) -> TeamStrategy:
"""Mock strategy interpreter — keyword matching fallback."""
text = raw_text.lower()
three_point_bias = 0.0
mid_range_bias = 0.0
at_rim_bias = 0.0
defensive_intensity = 0.0
pace_modifier = 1.0
sub_mod = 0.0
confidence = 0.5
# Shot selection keywords
if any(k in text for k in ("three", "bomb", "deep", "arc", "splash")):
three_point_bias = 12.0
confidence = 0.8
if any(k in text for k in ("paint", "rim", "drive", "attack", "inside")):
at_rim_bias = 12.0
confidence = 0.8
if any(k in text for k in ("mid-range", "midrange", "mid range", "jumper", "elbow")):
mid_range_bias = 12.0
confidence = 0.8
# Pace keywords
if any(k in text for k in ("fast", "run", "tempo", "push", "gun")):
pace_modifier = 0.8
confidence = 0.8
if any(k in text for k in ("slow", "deliberate", "half-court", "halfcourt", "patient")):
interpret_codegen_proposal function · python · L1274-L1327 (54 LOC)src/pinwheel/ai/interpreter.py
async def interpret_codegen_proposal(
raw_text: str,
api_key: str | None = None,
model: str = "claude-opus-4-6",
) -> ProposalInterpretation:
"""Route a proposal through the codegen council pipeline.
If ``api_key`` is provided, runs the full council (generate → AST validate
→ 3 reviews). Otherwise, uses the mock generator.
Returns a ProposalInterpretation with a single codegen EffectSpec.
"""
from pinwheel.ai.codegen_council import (
generate_codegen_effect_mock,
run_council_review,
)
if api_key:
spec, review = await run_council_review(
proposal_id="pending",
proposal_text=raw_text,
api_key=api_key,
model=model,
)
if spec is None:
# Council rejected — return low-confidence interpretation
flag_reasons = (
"; ".join(review.flag_reasons) if review.flag_reasons else "Council rejected"
)
return compute_pairwise_alignment function · python · L31-L89 (59 LOC)src/pinwheel/ai/report.py
def compute_pairwise_alignment(
votes: list[dict],
) -> list[dict[str, str | int | float]]:
"""Compute pairwise voting agreement between governors.
For each pair of governors who voted on at least one common proposal,
computes the number of shared proposals and the agreement percentage.
Args:
votes: List of vote dicts, each with at least "governor_id",
"proposal_id", and "vote" keys.
Returns:
List of dicts sorted by agreement_pct descending, each with:
- governor_a: str
- governor_b: str
- shared_proposals: int (number of proposals both voted on)
- agreed: int (number they voted the same way)
- agreement_pct: float (0.0-100.0)
"""
# Build a mapping: governor_id -> {proposal_id: vote_choice}
governor_votes: dict[str, dict[str, str]] = defaultdict(dict)
for v in votes:
gov_id = v.get("governor_id", "")
prop_id = v.get("proposal_id", "")
choice = v.get("vocompute_proposal_parameter_clustering function · python · L92-L154 (63 LOC)src/pinwheel/ai/report.py
def compute_proposal_parameter_clustering(
proposals: list[dict[str, str | int | float | None]],
history: list[dict[str, str | int | float | None]] | None = None,
) -> list[dict[str, str | int]]:
"""Detect proposal parameter clustering within a round and across history.
Groups proposals by parameter category prefix (e.g. "three_point_value"
-> "three_point", "elam_margin" -> "elam") and counts how many proposals
target each category.
When ``history`` is provided (proposals from earlier rounds), also checks
for recurring category focus across rounds.
Args:
proposals: Current round's proposals, each with an optional
"parameter" key.
history: Optional list of proposals from previous rounds, each with
an optional "parameter" key and optional "round_number" key.
Returns:
List of dicts sorted by count descending, each with:
- category: str (the parameter category prefix)
- count: int (Generated by Repobility's multi-pass static-analysis pipeline (https://repobility.com)
compute_governance_velocity function · python · L157-L225 (69 LOC)src/pinwheel/ai/report.py
def compute_governance_velocity(
current_round_proposals: int,
current_round_votes: int,
season_proposals_by_round: dict[int, int] | None = None,
season_votes_by_round: dict[int, int] | None = None,
) -> dict[str, str | float | int | bool]:
"""Assess governance velocity -- is this the most/least active window?
Compares the current round's activity to historical averages.
Args:
current_round_proposals: Number of proposals this round.
current_round_votes: Number of votes this round.
season_proposals_by_round: Optional dict mapping round_number to
proposal count across the full season.
season_votes_by_round: Optional dict mapping round_number to vote
count across the full season.
Returns:
Dict with:
- velocity_label: str ("peak", "high", "normal", "low", "silent")
- proposals_this_round: int
- votes_this_round: int
- avg_proposals_per_round: float
- avdetect_governance_blind_spots function · python · L228-L274 (47 LOC)src/pinwheel/ai/report.py
def detect_governance_blind_spots(
proposals: list[dict[str, str | int | float | None]],
rules_changed: list[dict[str, str | int | float | None]],
all_parameter_categories: list[str] | None = None,
) -> list[str]:
"""Identify areas of the game NOT being targeted by governance.
Compares what categories proposals target against the full parameter
space. Returns categories that have never been proposed against.
Args:
proposals: All proposals from the season (or recent window), each
with an optional "parameter" key.
rules_changed: All rule changes enacted, each with an optional
"parameter" key.
all_parameter_categories: Optional explicit list of all parameter
categories in the game. Defaults to a standard set.
Returns:
List of parameter category names that have NOT been targeted.
"""
if all_parameter_categories is None:
all_parameter_categories = [
"scoring",
categorize_parameter function · python · L328-L336 (9 LOC)src/pinwheel/ai/report.py
def categorize_parameter(param: str | None) -> str:
"""Map a governance parameter name to a human-readable category.
Returns the category name (e.g. "offense", "defense", "pace") or
"other" if the parameter is unknown or None.
"""
if param is None:
return "other"
return PARAMETER_CATEGORIES.get(param, "other")compute_category_distribution function · python · L339-L355 (17 LOC)src/pinwheel/ai/report.py
def compute_category_distribution(
proposals: list[dict[str, str | int | None]],
) -> dict[str, int]:
"""Count proposals per gameplay category.
Args:
proposals: List of dicts, each with at least a "parameter" key
(str or None).
Returns:
Dict mapping category name to count, sorted by count descending.
"""
counts: dict[str, int] = defaultdict(int)
for p in proposals:
cat = categorize_parameter(p.get("parameter")) # type: ignore[arg-type]
counts[cat] += 1
return dict(sorted(counts.items(), key=lambda kv: -kv[1]))compute_private_report_context function · python · L358-L541 (184 LOC)src/pinwheel/ai/report.py
async def compute_private_report_context(
repo: Repository,
governor_id: str,
season_id: str,
round_number: int,
) -> dict[str, object]:
"""Assemble rich context data for a governor's private report.
Computes the governor's proposal focus vs league-wide activity, surfaces
blind spots (categories they haven't touched but the league has changed),
and connects their voting record to actual outcomes.
"""
# 1. Governor's own proposals
gov_submitted = await repo.get_events_by_type_and_governor(
season_id=season_id,
governor_id=governor_id,
event_types=["proposal.submitted"],
)
gov_proposal_details: list[dict[str, str | int | None]] = []
for e in gov_submitted:
p_data = e.payload
interp = p_data.get("interpretation")
parameter = None
if interp and isinstance(interp, dict):
parameter = interp.get("parameter")
gov_proposal_details.append({
"raw_text": build_system_context function · python · L751-L850 (100 LOC)src/pinwheel/ai/report.py
def build_system_context(
round_data: dict,
narrative: NarrativeContext | None,
) -> dict[str, object]:
"""Compute system-level context for the simulation report.
Produces before/after comparisons, league-wide stats, rule correlation
data, and competitive balance metrics that the AI prompt or mock report
can use to surface invisible patterns.
Args:
round_data: Dict with ``games`` list and optional ``rule_changes``.
narrative: Optional NarrativeContext with standings, streaks, rules.
Returns:
Dict with keys: ``round_avg_total``, ``round_avg_margin``,
``all_games_close`` (margin <= 5 for every game),
``all_games_blowout`` (margin >= 15 for every game),
``standings_gap`` (wins diff between first and last),
``recent_rule_changes`` (list of {parameter, old_value, new_value, round_enacted}),
``leader_team``, ``trailer_team``, ``streaks_summary`` (list of
{team, streak} for streaks >= 3 generate_report_with_prompt function · python · L853-L882 (30 LOC)src/pinwheel/ai/report.py
async def generate_report_with_prompt(
prompt_template: str,
data: dict,
format_kwargs: dict,
report_type: str,
report_id_prefix: str,
round_number: int,
api_key: str,
governor_id: str = "",
season_id: str = "",
db_session: object | None = None,
) -> Report:
"""Generate a report using a specific prompt template (for A/B testing)."""
formatted = prompt_template.format(**format_kwargs)
content = await _call_claude(
system=formatted,
user_message=f"Generate a {report_type} report for this round.",
api_key=api_key,
call_type=f"report.{report_type}.ab",
season_id=season_id,
round_number=round_number,
db_session=db_session,
)
return Report(
id=f"{report_id_prefix}-{round_number}-{uuid.uuid4().hex[:8]}",
report_type=report_type,
round_number=round_number,
governor_id=governor_id,
content=content,
)generate_simulation_report function · python · L885-L922 (38 LOC)src/pinwheel/ai/report.py
async def generate_simulation_report(
round_data: dict,
season_id: str,
round_number: int,
api_key: str,
narrative: NarrativeContext | None = None,
db_session: object | None = None,
) -> Report:
"""Generate a simulation report using Claude.
Enriches the round data with system-level context (scoring trends,
rule correlations, competitive balance) before passing it to the prompt.
"""
# Compute system-level context and inject it into the round data
sys_ctx = build_system_context(round_data, narrative)
enriched_data = dict(round_data)
if sys_ctx:
enriched_data["system_context"] = sys_ctx
data_str = json.dumps(enriched_data, indent=2)
if narrative:
narrative_block = format_narrative_for_prompt(narrative)
data_str += f"\n\n--- Dramatic Context ---\n{narrative_block}"
content = await _call_claude(
system=SIMULATION_REPORT_PROMPT.format(round_data=data_str),
user_message="Generate a siRepobility's GitHub App fixes findings like these · https://github.com/apps/repobility-bot
generate_governance_report function · python · L925-L986 (62 LOC)src/pinwheel/ai/report.py
async def generate_governance_report(
governance_data: dict,
season_id: str,
round_number: int,
api_key: str,
narrative: NarrativeContext | None = None,
db_session: object | None = None,
) -> Report:
"""Generate a governance report using Claude.
Enriches the governance data with computed pairwise voting alignment,
parameter clustering, governance velocity, and blind spots before
passing it to the prompt, so Claude can surface coalition patterns
and system-level insights.
"""
# Compute pairwise alignment from votes and include in the data for Claude
enriched_data = dict(governance_data)
votes = enriched_data.get("votes", [])
if votes:
alignment = compute_pairwise_alignment(votes)
if alignment:
enriched_data["pairwise_voting_alignment"] = alignment
# Compute parameter clustering
proposals = enriched_data.get("proposals", [])
if proposals:
clustering = compute_proposal_paramet_compute_rule_correlations function · python · L989-L1052 (64 LOC)src/pinwheel/ai/report.py
def _compute_rule_correlations(
round_data: dict[str, object],
narrative: NarrativeContext,
) -> list[dict[str, object]]:
"""Compute rule-change correlation data from round games.
Returns a list of dicts with parameter, old/new values, avg total score
this round, rounds since change, and a human-readable summary.
Pre-computed data in round_data["rule_correlations"] takes precedence.
"""
precomputed = round_data.get("rule_correlations")
if isinstance(precomputed, list) and precomputed:
return precomputed
if not narrative.active_rule_changes:
return []
games = round_data.get("games", [])
if not isinstance(games, list) or not games:
return []
# Compute current round average total score
totals: list[float] = []
for g in games:
if isinstance(g, dict):
hs = g.get("home_score", 0)
aws = g.get("away_score", 0)
if isinstance(hs, (int, float)) and isinstance(aws, (i_compute_rule_correlations_with_history function · python · L1055-L1083 (29 LOC)src/pinwheel/ai/report.py
def _compute_rule_correlations_with_history(
round_data: dict[str, object],
narrative: NarrativeContext,
avg_total_before: float,
) -> list[dict[str, object]]:
"""Compute rule correlations with pre-computed before/after comparison.
Like _compute_rule_correlations but includes percentage change
when historical avg_total_before is provided.
"""
base = _compute_rule_correlations(round_data, narrative)
if not base or avg_total_before <= 0:
return base
for corr in base:
avg_after = float(corr.get("avg_total_after", 0))
pct = round(((avg_after - avg_total_before) / avg_total_before) * 100)
param = str(corr.get("parameter", "")).replace("_", " ")
new_val = corr.get("new_value")
direction = "up" if pct >= 0 else "down"
corr["avg_total_before"] = avg_total_before
corr["pct_change"] = abs(pct)
corr["summary"] = (
f"Since {param} changed to {new_val}: "
f"scorigenerate_private_report function · python · L1086-L1113 (28 LOC)src/pinwheel/ai/report.py
async def generate_private_report(
governor_data: dict,
governor_id: str,
season_id: str,
round_number: int,
api_key: str,
db_session: object | None = None,
) -> Report:
"""Generate a private report for a specific governor."""
content = await _call_claude(
system=PRIVATE_REPORT_PROMPT.format(
governor_id=governor_id,
governor_data=json.dumps(governor_data, indent=2),
),
user_message=f"Generate a private report for governor {governor_id}.",
api_key=api_key,
call_type="report.private",
season_id=season_id,
round_number=round_number,
db_session=db_session,
)
return Report(
id=f"r-priv-{round_number}-{uuid.uuid4().hex[:8]}",
report_type="private",
round_number=round_number,
governor_id=governor_id,
content=content,
)_call_claude function · python · L1116-L1167 (52 LOC)src/pinwheel/ai/report.py
async def _call_claude(
system: str,
user_message: str,
api_key: str,
call_type: str = "report",
season_id: str = "",
round_number: int | None = None,
db_session: object | None = None,
) -> str:
"""Make a Claude API call for report generation.
When ``db_session`` is provided, records token usage to the AI usage log.
"""
from pinwheel.ai.usage import (
cacheable_system,
extract_usage,
record_ai_usage,
track_latency,
)
model = "claude-sonnet-4-6"
try:
client = anthropic.AsyncAnthropic(api_key=api_key)
async with track_latency() as timing:
response = await client.messages.create(
model=model,
max_tokens=1500,
system=cacheable_system(system),
messages=[{"role": "user", "content": user_message}],
)
text = response.content[0].text
# Record usage if a DB session is available
ifgenerate_series_report function · python · L2030-L2062 (33 LOC)src/pinwheel/ai/report.py
async def generate_series_report(
series_data: dict,
season_id: str,
api_key: str,
db_session: object | None = None,
) -> Report:
"""Generate an AI-powered recap of a completed playoff series.
Args:
series_data: Dict with team names, game-by-game scores, series record,
series type (semifinal/finals), winner/loser info.
season_id: Season ID for usage tracking.
api_key: Anthropic API key.
db_session: Optional DB session for usage logging.
Returns:
A Report with report_type="series".
"""
data_str = json.dumps(series_data, indent=2)
content = await _call_claude(
system=SERIES_REPORT_PROMPT.format(series_data=data_str),
user_message="Write a recap of this completed playoff series.",
api_key=api_key,
call_type="report.series",
season_id=season_id,
db_session=db_session,
)
return Report(
id=f"r-series-{series_data.get('series_type', 'plagenerate_series_report_mock function · python · L2065-L2107 (43 LOC)src/pinwheel/ai/report.py
def generate_series_report_mock(series_data: dict) -> Report:
"""Generate a mock series recap for testing.
Args:
series_data: Dict with team names, game-by-game scores, series record,
series type, winner/loser info.
Returns:
A Report with report_type="series" and deterministic content.
"""
winner = series_data.get("winner_name", "Winner")
loser = series_data.get("loser_name", "Loser")
record = series_data.get("record", "?-?")
series_type = series_data.get("series_type", "playoff")
games = series_data.get("games", [])
lines: list[str] = []
if series_type == "finals":
lines.append(
f"The championship finals are over. {winner} claimed the title "
f"with a {record} series victory over {loser}."
)
else:
lines.append(
f"{winner} advanced past {loser} in a {record} semifinal series."
)
if games:
last_game = games[-1]
lines.appgenerate_state_of_the_league function · python · L2146-L2182 (37 LOC)src/pinwheel/ai/report.py
async def generate_state_of_the_league(
league_data: dict,
season_id: str,
round_number: int,
api_key: str,
narrative: NarrativeContext | None = None,
db_session: object | None = None,
) -> Report:
"""Generate a State of the League report using Claude.
Triggered every 7 rounds (one full round-robin) as a periodic zoom-out
on standings, rule drift, power dynamics, and emerging narratives.
"""
data_str = json.dumps(league_data, indent=2)
if narrative:
narrative_block = format_narrative_for_prompt(narrative)
data_str += f"\n\n--- Dramatic Context ---\n{narrative_block}"
content = await _call_claude(
system=STATE_OF_THE_LEAGUE_PROMPT.format(
league_data=data_str
),
user_message=(
"Write a State of the League address "
"for this point in the season."
),
api_key=api_key,
call_type="report.state_of_the_league",
season_id=season_id,
Repobility analyzer · published findings · https://repobility.com
generate_state_of_the_league_mock function · python · L2185-L2245 (61 LOC)src/pinwheel/ai/report.py
def generate_state_of_the_league_mock(
league_data: dict,
season_id: str,
round_number: int,
narrative: NarrativeContext | None = None,
) -> Report:
"""Generate a mock State of the League report for testing."""
standings = league_data.get("standings", [])
rule_changes = league_data.get("rule_changes", [])
lines: list[str] = []
lines.append(f"State of the League — Round {round_number}.")
if standings:
leader = standings[0] if standings else {}
leader_name = leader.get("team_name", "Unknown")
leader_wins = leader.get("wins", 0)
leader_losses = leader.get("losses", 0)
lines.append(
f"{leader_name} leads the league at "
f"{leader_wins}-{leader_losses}."
)
if len(standings) >= 2:
trailer = standings[-1]
trailer_name = trailer.get("team_name", "Unknown")
gap = leader_wins - trailer.get("wins", 0)
if gap > 0:
generate_tiebreaker_report function · python · L2277-L2305 (29 LOC)src/pinwheel/ai/report.py
async def generate_tiebreaker_report(
tiebreaker_data: dict,
season_id: str,
round_number: int,
api_key: str,
db_session: object | None = None,
) -> Report:
"""Generate a tiebreaker report using Claude.
Triggered after tiebreaker games resolve.
"""
data_str = json.dumps(tiebreaker_data, indent=2)
content = await _call_claude(
system=TIEBREAKER_REPORT_PROMPT.format(
tiebreaker_data=data_str
),
user_message="Write a tiebreaker report for this round.",
api_key=api_key,
call_type="report.tiebreaker",
season_id=season_id,
round_number=round_number,
db_session=db_session,
)
return Report(
id=f"r-tb-{round_number}-{uuid.uuid4().hex[:8]}",
report_type="tiebreaker",
round_number=round_number,
content=content,
)generate_tiebreaker_report_mock function · python · L2308-L2367 (60 LOC)src/pinwheel/ai/report.py
def generate_tiebreaker_report_mock(
tiebreaker_data: dict,
season_id: str,
round_number: int,
) -> Report:
"""Generate a mock tiebreaker report for testing."""
tied_teams = tiebreaker_data.get("tied_teams", [])
games = tiebreaker_data.get("games", [])
rule_changes = tiebreaker_data.get("rule_changes", [])
lines: list[str] = []
if tied_teams:
team_names = [
t.get("team_name", "Unknown") for t in tied_teams
]
lines.append(
"The regular season ended in a dead heat "
f"between {', '.join(team_names)}."
)
else:
lines.append(
"Tiebreaker games determined the final "
"playoff seeding."
)
if rule_changes:
lines.append(
"Governors used the extra governance window — "
f"{len(rule_changes)} rule changes were enacted "
"before the tiebreaker tip-off."
)
else:
lines.append(
generate_offseason_report function · python · L2398-L2431 (34 LOC)src/pinwheel/ai/report.py
async def generate_offseason_report(
offseason_data: dict,
season_id: str,
api_key: str,
db_session: object | None = None,
) -> Report:
"""Generate an offseason report using Claude.
Triggered at season transitions when the offseason governance
window closes.
"""
data_str = json.dumps(offseason_data, indent=2)
content = await _call_claude(
system=OFFSEASON_REPORT_PROMPT.format(
offseason_data=data_str
),
user_message=(
"Write an offseason report for this "
"season transition."
),
api_key=api_key,
call_type="report.offseason",
season_id=season_id,
db_session=db_session,
)
return Report(
id=(
f"r-offseason-{season_id[:8]}-"
f"{uuid.uuid4().hex[:8]}"
),
report_type="offseason",
round_number=0,
content=content,
)generate_offseason_report_mock function · python · L2434-L2499 (66 LOC)src/pinwheel/ai/report.py
def generate_offseason_report_mock(
offseason_data: dict,
season_id: str,
) -> Report:
"""Generate a mock offseason report for testing."""
rules_carried = offseason_data.get("rules_carried", [])
rules_reset = offseason_data.get("rules_reset", [])
offseason_proposals = offseason_data.get(
"offseason_proposals", 0
)
champion = offseason_data.get("champion_team_name", "")
lines: list[str] = []
if champion:
lines.append(
f"The {champion} era ends — or continues. "
f"The offseason governance window has closed."
)
else:
lines.append(
"The offseason governance window has closed."
)
if rules_carried:
lines.append(
f"{len(rules_carried)} rules carried forward "
"into the new season, preserving the governance "
"legacy of the outgoing campaign."
)
else:
lines.append(
"The ruleset was reset to defgenerate_season_memorial function · python · L2577-L2701 (125 LOC)src/pinwheel/ai/report.py
async def generate_season_memorial(
memorial_data: dict,
season_id: str,
api_key: str,
db_session: object | None = None,
) -> dict:
"""Generate AI narrative sections for a season memorial.
Makes 4 concurrent Claude calls for the narrative sections:
season_narrative, championship_recap, champion_profile, governance_legacy.
Args:
memorial_data: Dict from gather_memorial_data() with computed sections.
season_id: Season being memorialized.
api_key: Anthropic API key.
db_session: Optional DB session for usage logging.
Returns:
Updated memorial_data dict with AI narratives filled in.
"""
import asyncio
# Prepare context for each prompt
season_context = json.dumps(
{
"awards": memorial_data.get("awards", []),
"statistical_leaders": memorial_data.get("statistical_leaders", {}),
"key_moments": memorial_data.get("key_moments", []),
"head_to_head"generate_season_memorial_mock function · python · L2704-L2789 (86 LOC)src/pinwheel/ai/report.py
def generate_season_memorial_mock(memorial_data: dict) -> dict:
"""Generate mock AI narrative sections for testing.
Fills in reasonable static content for each narrative section
based on available computed data.
Args:
memorial_data: Dict from gather_memorial_data() with computed sections.
Returns:
Updated memorial_data dict with mock narratives filled in.
"""
awards = memorial_data.get("awards", [])
key_moments = memorial_data.get("key_moments", [])
rule_timeline = memorial_data.get("rule_timeline", [])
leaders = memorial_data.get("statistical_leaders", {})
# Season narrative
parts = ["Another season in the books for Pinwheel Fates."]
if key_moments:
closest = [m for m in key_moments if m.get("moment_type") == "closest_game"]
if closest:
m = closest[0]
parts.append(
f"The closest game of the season saw {m.get('home_team_name', '?')} "
f"edge {mNameResolver.__init__ method · python · L121-L135 (15 LOC)src/pinwheel/ai/search.py
def __init__(
self,
teams: list[TeamRow],
hoopers: list[HooperRow] | None = None,
) -> None:
self._teams: dict[str, TeamRow] = {}
self._hoopers: dict[str, HooperRow] = {}
self._team_id_to_name: dict[str, str] = {}
for t in teams:
self._teams[t.name.lower()] = t
self._team_id_to_name[t.id] = t.name
for h in hoopers or []:
self._hoopers[h.name.lower()] = hRepobility — same analyzer, your code, free for public repos · /scan/
NameResolver.resolve_team method · python · L137-L147 (11 LOC)src/pinwheel/ai/search.py
def resolve_team(self, name: str) -> TeamRow | None:
"""Resolve a team name (exact, case-insensitive, then partial match)."""
lowered = name.lower().strip()
# Exact match
if lowered in self._teams:
return self._teams[lowered]
# Partial/substring match
for key, team in self._teams.items():
if lowered in key or key in lowered:
return team
return NoneNameResolver.resolve_hooper method · python · L149-L157 (9 LOC)src/pinwheel/ai/search.py
def resolve_hooper(self, name: str) -> HooperRow | None:
"""Resolve a hooper name (exact, case-insensitive, then partial match)."""
lowered = name.lower().strip()
if lowered in self._hoopers:
return self._hoopers[lowered]
for key, hooper in self._hoopers.items():
if lowered in key or key in lowered:
return hooper
return Noneparse_query_mock function · python · L169-L230 (62 LOC)src/pinwheel/ai/search.py
def parse_query_mock(question: str) -> QueryPlan:
"""Parse a natural language question into a QueryPlan using keyword matching.
No AI call required. Handles the most common question patterns.
"""
q = question.lower().strip()
# Head to head: "X vs Y", "X against Y", "head to head"
vs_match = re.search(r"(.+?)\s+(?:vs\.?|versus|against)\s+(.+)", q)
if vs_match:
return QueryPlan(
query_type="head_to_head",
team_a_name=vs_match.group(1).strip(),
team_b_name=vs_match.group(2).strip(),
)
if "head to head" in q:
return QueryPlan(query_type="head_to_head")
# Stat leaders: "who leads", "top", "best", "most", "leader"
if any(kw in q for kw in ("who leads", "top", "best", "most", "leader")):
stat = _extract_stat(q)
limit = _extract_limit(q)
return QueryPlan(query_type="stat_leaders", stat=stat, limit=limit)
# Standings
if any(kw in q for kw in ("standings", "rank_extract_stat function · python · L233-L238 (6 LOC)src/pinwheel/ai/search.py
def _extract_stat(question: str) -> str:
"""Extract a stat name from the question, defaulting to 'points'."""
for alias, canonical in STAT_ALIASES.items():
if alias in question:
return canonical
return "points"_extract_limit function · python · L241-L246 (6 LOC)src/pinwheel/ai/search.py
def _extract_limit(question: str) -> int:
"""Extract a numeric limit from the question, defaulting to 5."""
match = re.search(r"\btop\s+(\d+)\b", question)
if match:
return min(int(match.group(1)), 25)
return 5_extract_team_name_from_question function · python · L249-L265 (17 LOC)src/pinwheel/ai/search.py
def _extract_team_name_from_question(question: str) -> str | None:
"""Try to extract a team name from a question.
Very rough heuristic: returns words after certain prepositions/verbs.
The NameResolver does the real matching; this just narrows the search.
"""
for pattern in (
r"(?:for|of|about|the)\s+(?:the\s+)?(.+?)(?:\?|$|'s)",
r"(?:for|of|about|the)\s+(.+?)(?:\?|$)",
):
match = re.search(pattern, question)
if match:
candidate = match.group(1).strip().rstrip("?. ")
# Skip if the candidate is just a stat word
if candidate and candidate not in STAT_ALIASES and len(candidate) > 2:
return candidate
return None_extract_hooper_name_from_question function · python · L268-L278 (11 LOC)src/pinwheel/ai/search.py
def _extract_hooper_name_from_question(question: str) -> str | None:
"""Try to extract a hooper name from a question."""
for pattern in (
r"stats (?:for|on)\s+(.+?)(?:\?|$)",
r"how (?:is|'s)\s+(.+?)\s+doing",
r"how (?:is|'s)\s+(.+?)(?:\?|$)",
):
match = re.search(pattern, question)
if match:
return match.group(1).strip().rstrip("?. ")
return Noneparse_query_ai function · python · L334-L369 (36 LOC)src/pinwheel/ai/search.py
async def parse_query_ai(
question: str,
api_key: str,
team_names: list[str],
hooper_names: list[str],
) -> QueryPlan:
"""Parse a question using Claude API. Falls back to mock on failure."""
from pinwheel.ai.usage import cacheable_system, pydantic_to_response_format
system = SEARCH_PARSER_SYSTEM_PROMPT.format(
team_names=", ".join(team_names) if team_names else "(none yet)",
hooper_names=", ".join(hooper_names) if hooper_names else "(none yet)",
)
model = "claude-sonnet-4-6"
try:
client = anthropic.AsyncAnthropic(api_key=api_key)
response = await client.messages.create(
model=model,
max_tokens=300,
system=cacheable_system(system),
messages=[{"role": "user", "content": question}],
output_config=pydantic_to_response_format(QueryPlan, "query_plan"),
)
text = response.content[0].text.strip()
# Fallback: handle markdown code fences (bGenerated by Repobility's multi-pass static-analysis pipeline (https://repobility.com)
execute_query function · python · L386-L458 (73 LOC)src/pinwheel/ai/search.py
async def execute_query(
plan: QueryPlan,
repo: object,
season_id: str,
resolver: NameResolver,
) -> QueryResult:
"""Execute a QueryPlan against the repository and return raw data.
Args:
plan: The parsed query plan.
repo: A Repository instance (typed as object to avoid circular import).
season_id: Active season ID.
resolver: A NameResolver loaded with teams/hoopers for this season.
"""
from pinwheel.db.repository import Repository
r: Repository = repo # type: ignore[assignment]
try:
if plan.query_type == "standings":
return await _exec_standings(r, season_id, resolver)
elif plan.query_type == "team_record":
return await _exec_team_record(r, season_id, resolver, plan.team_name)
elif plan.query_type == "last_game":
return await _exec_last_game(r, season_id, resolver, plan.team_name)
elif plan.query_type == "stat_leaders":
stat = pl_exec_standings function · python · L461-L485 (25 LOC)src/pinwheel/ai/search.py
async def _exec_standings(
repo: object,
season_id: str,
resolver: NameResolver,
) -> QueryResult:
"""Execute a standings query."""
from pinwheel.core.scheduler import compute_standings
from pinwheel.db.repository import Repository
r: Repository = repo # type: ignore[assignment]
all_games = await r.get_all_game_results_for_season(season_id)
results = [
{
"home_team_id": g.home_team_id,
"away_team_id": g.away_team_id,
"home_score": g.home_score,
"away_score": g.away_score,
"winner_team_id": g.winner_team_id,
}
for g in all_games
]
standings = compute_standings(results)
for s in standings:
s["team_name"] = resolver.team_name(s["team_id"])
return QueryResult(query_type="standings", data={"standings": standings})_exec_team_record function · python · L488-L538 (51 LOC)src/pinwheel/ai/search.py
async def _exec_team_record(
repo: object,
season_id: str,
resolver: NameResolver,
team_name: str | None,
) -> QueryResult:
"""Execute a team record query."""
from pinwheel.core.scheduler import compute_standings
from pinwheel.db.repository import Repository
r: Repository = repo # type: ignore[assignment]
if not team_name:
return QueryResult(
query_type="team_record",
error="Which team? Try asking like 'what is the Thorns record?'",
)
team = resolver.resolve_team(team_name)
if not team:
return QueryResult(
query_type="team_record",
error=f"Could not find a team matching '{team_name}'.",
)
all_games = await r.get_all_game_results_for_season(season_id)
results = [
{
"home_team_id": g.home_team_id,
"away_team_id": g.away_team_id,
"home_score": g.home_score,
"away_score": g.away_score,
"w_exec_last_game function · python · L541-L580 (40 LOC)src/pinwheel/ai/search.py
async def _exec_last_game(
repo: object,
season_id: str,
resolver: NameResolver,
team_name: str | None,
) -> QueryResult:
"""Execute a last game query."""
from pinwheel.db.repository import Repository
r: Repository = repo # type: ignore[assignment]
if team_name:
team = resolver.resolve_team(team_name)
if not team:
return QueryResult(
query_type="last_game",
error=f"Could not find a team matching '{team_name}'.",
)
games = await r.get_games_for_team(season_id, team.id)
else:
games = await r.get_all_game_results_for_season(season_id)
if not games:
return QueryResult(
query_type="last_game",
data={"message": "No games have been played yet."},
)
last = games[-1]
return QueryResult(
query_type="last_game",
data={
"home_team": resolver.team_name(last.home_team_id),
"away_tea_exec_stat_leaders function · python · L583-L609 (27 LOC)src/pinwheel/ai/search.py
async def _exec_stat_leaders(
repo: object,
season_id: str,
resolver: NameResolver,
stat: str,
limit: int,
) -> QueryResult:
"""Execute a stat leaders query."""
from pinwheel.db.repository import Repository
r: Repository = repo # type: ignore[assignment]
leaders = await r.get_stat_leaders(season_id, stat, limit=limit)
# Resolve hooper names
for entry in leaders:
hooper = await r.get_hooper(entry["hooper_id"])
if hooper:
entry["hooper_name"] = hooper.name
entry["team_name"] = resolver.team_name(hooper.team_id)
else:
entry["hooper_name"] = entry["hooper_id"]
entry["team_name"] = "Unknown"
return QueryResult(
query_type="stat_leaders",
data={"stat": stat, "leaders": leaders},
)_exec_hooper_stats function · python · L612-L676 (65 LOC)src/pinwheel/ai/search.py
async def _exec_hooper_stats(
repo: object,
season_id: str,
resolver: NameResolver,
hooper_name: str | None,
) -> QueryResult:
"""Execute a hooper stats query."""
from pinwheel.db.repository import Repository
r: Repository = repo # type: ignore[assignment]
if not hooper_name:
return QueryResult(
query_type="hooper_stats",
error="Which hooper? Try asking like 'stats for Rivera'.",
)
hooper = resolver.resolve_hooper(hooper_name)
if not hooper:
return QueryResult(
query_type="hooper_stats",
error=f"Could not find a hooper matching '{hooper_name}'.",
)
box_scores = await r.get_box_scores_for_hooper(hooper.id)
if not box_scores:
return QueryResult(
query_type="hooper_stats",
data={
"hooper_name": hooper.name,
"team_name": resolver.team_name(hooper.team_id),
"games_played": 0,
_exec_head_to_head function · python · L679-L739 (61 LOC)src/pinwheel/ai/search.py
async def _exec_head_to_head(
repo: object,
season_id: str,
resolver: NameResolver,
team_a_name: str | None,
team_b_name: str | None,
) -> QueryResult:
"""Execute a head-to-head query."""
from pinwheel.db.repository import Repository
r: Repository = repo # type: ignore[assignment]
if not team_a_name or not team_b_name:
return QueryResult(
query_type="head_to_head",
error="Please specify two teams, like 'Thorns vs Voltage'.",
)
team_a = resolver.resolve_team(team_a_name)
team_b = resolver.resolve_team(team_b_name)
if not team_a:
return QueryResult(
query_type="head_to_head",
error=f"Could not find a team matching '{team_a_name}'.",
)
if not team_b:
return QueryResult(
query_type="head_to_head",
error=f"Could not find a team matching '{team_b_name}'.",
)
games = await r.get_head_to_head(season_id, team_a.id, t_exec_schedule function · python · L742-L772 (31 LOC)src/pinwheel/ai/search.py
async def _exec_schedule(
repo: object,
season_id: str,
resolver: NameResolver,
team_name: str | None,
) -> QueryResult:
"""Execute a schedule query."""
from pinwheel.db.repository import Repository
r: Repository = repo # type: ignore[assignment]
schedule = await r.get_full_schedule(season_id)
entries = []
for s in schedule:
if team_name:
team = resolver.resolve_team(team_name)
if team and s.home_team_id != team.id and s.away_team_id != team.id:
continue
entries.append(
{
"round": s.round_number,
"home_team": resolver.team_name(s.home_team_id),
"away_team": resolver.team_name(s.away_team_id),
"status": s.status,
}
)
return QueryResult(
query_type="schedule",
data={"schedule": entries},
)Repobility's GitHub App fixes findings like these · https://github.com/apps/repobility-bot
_exec_rules_current function · python · L775-L792 (18 LOC)src/pinwheel/ai/search.py
async def _exec_rules_current(
repo: object,
season_id: str,
) -> QueryResult:
"""Execute a current rules query."""
from pinwheel.db.repository import Repository
r: Repository = repo # type: ignore[assignment]
season = await r.get_season(season_id)
if not season or not season.current_ruleset:
return QueryResult(
query_type="rules_current",
data={"rules": {}},
)
return QueryResult(
query_type="rules_current",
data={"rules": season.current_ruleset},
)_exec_team_roster function · python · L795-L836 (42 LOC)src/pinwheel/ai/search.py
async def _exec_team_roster(
repo: object,
season_id: str,
resolver: NameResolver,
team_name: str | None,
) -> QueryResult:
"""Execute a team roster query."""
from pinwheel.db.repository import Repository
r: Repository = repo # type: ignore[assignment]
if not team_name:
# Return all teams
teams = await r.get_teams_for_season(season_id)
all_rosters = []
for t in teams:
hoopers = [
{"name": h.name, "archetype": h.archetype}
for h in t.hoopers
]
all_rosters.append({"team_name": t.name, "hoopers": hoopers})
return QueryResult(
query_type="team_roster",
data={"rosters": all_rosters},
)
team = resolver.resolve_team(team_name)
if not team:
return QueryResult(
query_type="team_roster",
error=f"Could not find a team matching '{team_name}'.",
)
hoopers_list = await r.get_exec_proposals function · python · L839-L851 (13 LOC)src/pinwheel/ai/search.py
async def _exec_proposals(
repo: object,
season_id: str,
) -> QueryResult:
"""Execute a proposals query."""
from pinwheel.db.repository import Repository
r: Repository = repo # type: ignore[assignment]
proposals = await r.get_all_proposals(season_id)
return QueryResult(
query_type="proposals",
data={"proposals": proposals},
)