← back to djacobs__Pinwheel

Function bodies 688 total

All specs Real LLM only Function bodies
_run_evals function · python · L712-L834 (123 LOC)
src/pinwheel/core/game_loop.py
async def _run_evals(
    repo: Repository,
    season_id: str,
    round_number: int,
    reports: list[Report],
    game_summaries: list[dict],
    teams_cache: dict,
    api_key: str = "",
) -> None:
    """Run automated evals after report generation. Non-blocking."""
    from pinwheel.evals.behavioral import compute_report_impact_rate
    from pinwheel.evals.grounding import GroundingContext, check_grounding
    from pinwheel.evals.prescriptive import scan_prescriptive

    # Build grounding context
    team_data = [{"name": t.name} for t in teams_cache.values()]
    hooper_data = []
    for t in teams_cache.values():
        for h in t.hoopers:
            hooper_data.append({"name": h.name})
    season = await repo.get_season(season_id)
    ruleset_dict = (season.current_ruleset if season else None) or {}
    context = GroundingContext(
        team_names=[d["name"] for d in team_data],
        agent_names=[d["name"] for d in hooper_data],
        rule_params=list((ruleset_dict o
tally_pending_governance function · python · L837-L1037 (201 LOC)
src/pinwheel/core/game_loop.py
async def tally_pending_governance(
    repo: Repository,
    season_id: str,
    round_number: int,
    ruleset: RuleSet,
    event_bus: EventBus | None = None,
    effect_registry: EffectRegistry | None = None,
    meta_store: MetaStore | None = None,
    skip_deferral: bool = False,
) -> tuple[RuleSet, list[VoteTally], dict]:
    """Tally all pending proposals and enact passing rule changes.

    Standalone function — can run with or without game simulation.
    When ``effect_registry`` is provided, passing proposals that contain
    v2 effects (meta_mutation, hook_callback, narrative) will have those
    effects registered in the registry and persisted to the event store.
    When ``meta_store`` is provided, fires ``gov.pre`` and ``gov.post``
    hooks around the governance tally for any registered effects.
    When ``skip_deferral`` is True, the minimum voting period is bypassed
    (used for season-close catch-up tallies).
    Returns (updated_ruleset, tallies, governance_data).
_get_series_games function · python · L2084-L2114 (31 LOC)
src/pinwheel/core/game_loop.py
async def _get_series_games(
    repo: Repository,
    season_id: str,
    team_a_id: str,
    team_b_id: str,
) -> list[dict]:
    """Get all playoff games between two teams, ordered chronologically.

    Returns a list of dicts with home/away team ids, scores, round number.
    """
    playoff_schedule = await repo.get_full_schedule(season_id, phase="playoff")
    playoff_rounds = {s.round_number for s in playoff_schedule}
    all_games = await repo.get_all_games(season_id)

    pair = frozenset({team_a_id, team_b_id})
    series_games: list[dict] = []
    for g in sorted(all_games, key=lambda g: (g.round_number, g.matchup_index)):
        if g.round_number not in playoff_rounds:
            continue
        if frozenset({g.home_team_id, g.away_team_id}) == pair:
            series_games.append(
                {
                    "round_number": g.round_number,
                    "home_team_id": g.home_team_id,
                    "away_team_id": g.away_team_id,
                 
_generate_series_reports function · python · L2117-L2313 (197 LOC)
src/pinwheel/core/game_loop.py
async def _generate_series_reports(
    repo: Repository,
    season_id: str,
    deferred_events: list[tuple[str, dict]],
    teams_cache: dict[str, Team],
    api_key: str = "",
) -> list[dict]:
    """Generate series reports for any completed series in deferred events.

    Scans deferred_events for ``season.semifinals_complete`` and
    ``season.playoffs_complete`` events, gathers game data for each
    completed series, generates an AI recap, and stores it.

    Returns list of report event dicts for downstream publishing.
    """
    report_events: list[dict] = []

    for event_type, event_data in deferred_events:
        if event_type == "season.semifinals_complete":
            # Each semi series that just completed
            for semi in event_data.get("semi_series", []):
                winner_id = semi.get("winner_id", "")
                loser_id = semi.get("loser_id", "")
                if not winner_id or not loser_id:
                    continue

                winn
step_round function · python · L2797-L2872 (76 LOC)
src/pinwheel/core/game_loop.py
async def step_round(
    repo: Repository,
    season_id: str,
    round_number: int,
    event_bus: EventBus | None = None,
    api_key: str = "",
    governance_interval: int = 1,
    suppress_spoiler_events: bool = False,
) -> RoundResult:
    """Execute one complete round of the game loop.

    Returns a RoundResult with game results, governance outcomes, and reports.
    Delegates to the three phase functions but keeps everything in one session
    (backward-compatible single-session behavior).
    """
    start = time.monotonic()
    logger.info("round_start season=%s round=%d", season_id, round_number)

    phase1_start = time.perf_counter()
    sim = await _phase_simulate_and_govern(
        repo,
        season_id,
        round_number,
        event_bus=event_bus,
        governance_interval=governance_interval,
        suppress_spoiler_events=suppress_spoiler_events,
    )
    phase1_ms = (time.perf_counter() - phase1_start) * 1000
    logger.info(
        "phase_timing pha
step_round_multisession function · python · L2875-L2973 (99 LOC)
src/pinwheel/core/game_loop.py
async def step_round_multisession(
    engine: object,
    season_id: str,
    round_number: int,
    event_bus: EventBus | None = None,
    api_key: str = "",
    governance_interval: int = 1,
    suppress_spoiler_events: bool = False,
) -> RoundResult:
    """Execute one round with separate DB sessions per phase.

    Releases the SQLite write lock between phases so Discord commands
    (/join, /propose, /vote) can write freely during slow AI calls.

    Lock timeline:
        Session 1 (~2-3s): simulate games, store results, tally governance
           [LOCK RELEASED]
        AI calls (~30-90s): commentary, highlights, reports (NO session open)
           [LOCK RELEASED]
        Session 2 (~1-2s): store reports, run evals, season progression
           [LOCK RELEASED]

    The ``engine`` parameter is typed as ``object`` to avoid importing
    AsyncEngine at module level; callers pass an ``AsyncEngine`` instance.
    """
    from pinwheel.db.engine import get_session as _get_session
RoundResult.__init__ method · python · L2979-L3011 (33 LOC)
src/pinwheel/core/game_loop.py
    def __init__(
        self,
        round_number: int,
        games: list[dict],
        reports: list[Report],
        tallies: list[VoteTally],
        game_results: list[GameResult] | None = None,
        game_row_ids: list[str] | None = None,
        teams_cache: dict | None = None,
        governance_summary: dict | None = None,
        season_complete: bool = False,
        final_standings: list[dict] | None = None,
        playoff_bracket: list[dict] | None = None,
        playoffs_complete: bool = False,
        finals_matchup: dict | None = None,
        report_events: list[dict] | None = None,
        deferred_season_events: list[tuple[str, dict]] | None = None,
    ) -> None:
        self.round_number = round_number
        self.games = games
        self.reports = reports
        self.tallies = tallies
        self.game_results = game_results or []
        self.game_row_ids = game_row_ids or []
        self.teams_cache = teams_cache or {}
        self.governance_summar
Repobility's GitHub App fixes findings like these · https://github.com/apps/repobility-bot
remove_invisible_chars function · python · L48-L66 (19 LOC)
src/pinwheel/core/governance.py
def remove_invisible_chars(text: str) -> str:
    """Remove zero-width, directional, and invisible Unicode characters.

    Covers: C0/C1 control characters, zero-width spaces/joiners,
    directional overrides and isolates, invisible operators, BOM,
    and other formatting characters that could embed hidden instructions.
    """
    return re.sub(
        r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f"
        r"\u200b-\u200f"          # zero-width space, joiners, directional marks
        r"\u2028-\u202f"          # line/paragraph separators, directional overrides
        r"\u2060-\u2064"          # invisible operators (word joiner, etc.)
        r"\u2066-\u206f"          # directional isolates, deprecated formatting
        r"\ufeff"                 # BOM / zero-width no-break space
        r"\ufff9-\ufffb"         # interlinear annotation anchors
        r"]",
        "",
        text,
    )
strip_prompt_markers function · python · L69-L85 (17 LOC)
src/pinwheel/core/governance.py
def strip_prompt_markers(text: str) -> str:
    """Remove prompt injection markers — both plain-text and XML-style.

    Strips patterns like "System:", "Human:", "Assistant:" (case-insensitive)
    that could trick an LLM into treating user text as system instructions.
    Also strips triple-backtick code fences used to break out of delimiters.
    """
    # Plain-text role markers (case-insensitive, at word boundary)
    text = re.sub(
        r"\b(System|Human|User|Assistant|Claude)\s*:",
        "",
        text,
        flags=re.IGNORECASE,
    )
    # Triple-backtick fences (could break out of markdown delimiters)
    text = text.replace("```", "")
    return text
sanitize_text function · python · L88-L103 (16 LOC)
src/pinwheel/core/governance.py
def sanitize_text(raw: str, max_length: int = 500) -> str:
    """Strip dangerous content from governor-submitted text.

    Pipeline: invisible chars -> HTML tags -> prompt markers -> whitespace -> length.
    This is the single entry point for all governor-submitted text sanitization.
    """
    # 1. Strip invisible Unicode (zero-width, directional, formatting)
    text = remove_invisible_chars(raw)
    # 2. Strip HTML/XML tags
    text = re.sub(r"<[^>]+>", "", text)
    # 3. Strip prompt injection markers (plain-text and XML-style)
    text = strip_prompt_markers(text)
    # 4. Collapse whitespace
    text = re.sub(r"\s+", " ", text).strip()
    # 5. Enforce length
    return text[:max_length]
detect_tier function · python · L119-L173 (55 LOC)
src/pinwheel/core/governance.py
def detect_tier(interpretation: RuleInterpretation, ruleset: RuleSet) -> int:
    """Determine the governance tier of an interpreted proposal.

    Tiers 1-4 are parameter changes. Higher tiers need higher vote thresholds.
    """
    if interpretation.parameter is None:
        return 5  # Game Effect or uninterpretable
    param = interpretation.parameter
    tier1 = {
        "quarter_minutes",
        "shot_clock_seconds",
        "three_point_value",
        "two_point_value",
        "free_throw_value",
        "personal_foul_limit",
        "team_foul_bonus_threshold",
        "three_point_distance",
        "elam_trigger_quarter",
        "elam_margin",
        "halftime_stamina_recovery",
        "safety_cap_possessions",
        "turnover_rate_modifier",
        "foul_rate_modifier",
        "offensive_rebound_weight",
        "stamina_drain_rate",
        "dead_ball_time_seconds",
    }
    tier2 = {
        "max_shot_share",
        "min_pass_per_possession",
        "home_
detect_tier_v2 function · python · L176-L220 (45 LOC)
src/pinwheel/core/governance.py
def detect_tier_v2(interpretation: ProposalInterpretation, ruleset: RuleSet) -> int:
    """Determine governance tier from a V2 ProposalInterpretation.

    Examines the effects list directly instead of relying on the legacy
    ``to_rule_interpretation()`` conversion (which loses non-parameter effects).

    Tier rules:
    - ``parameter_change`` → reuse per-parameter tier logic (1-4)
    - ``hook_callback`` / ``meta_mutation`` / ``move_grant`` → Tier 3
    - Only ``narrative`` effects → Tier 2
    - No effects / ``injection_flagged`` / ``rejection_reason`` → Tier 5
    - Compound proposals: highest tier wins
    """
    if interpretation.injection_flagged or interpretation.rejection_reason:
        return 5

    effects = interpretation.effects
    if not effects:
        return 5

    tiers: list[int] = []
    for effect in effects:
        if effect.effect_type == "parameter_change" and effect.parameter:
            # Reuse the legacy per-parameter tier lookup
            legacy = 
token_cost_for_tier function · python · L223-L229 (7 LOC)
src/pinwheel/core/governance.py
def token_cost_for_tier(tier: int) -> int:
    """Higher tiers cost more PROPOSE tokens."""
    if tier <= 4:
        return 1
    if tier <= 6:
        return 2
    return 3
vote_threshold_for_tier function · python · L232-L240 (9 LOC)
src/pinwheel/core/governance.py
def vote_threshold_for_tier(tier: int, base_threshold: float = 0.5) -> float:
    """Higher tiers need supermajority."""
    if tier <= 2:
        return base_threshold
    if tier <= 4:
        return max(base_threshold, 0.6)
    if tier <= 6:
        return 0.67
    return 0.75
submit_proposal function · python · L246-L320 (75 LOC)
src/pinwheel/core/governance.py
async def submit_proposal(
    repo: Repository,
    governor_id: str,
    team_id: str,
    season_id: str,
    window_id: str,
    raw_text: str,
    interpretation: RuleInterpretation,
    ruleset: RuleSet,
    *,
    token_already_spent: bool = False,
    interpretation_v2: ProposalInterpretation | None = None,
) -> Proposal:
    """Submit a proposal. Deducts PROPOSE token(s) via event store.

    If ``token_already_spent`` is True, the token was deducted at propose-time
    (before the confirm UI) to prevent race conditions, so the token.spent
    event is skipped here.

    When ``interpretation_v2`` is provided, tier detection uses the V2 effects
    list instead of the legacy parameter-based tier lookup.
    """
    sanitized = sanitize_text(raw_text)
    if interpretation_v2 is not None:
        tier = detect_tier_v2(interpretation_v2, ruleset)
    else:
        tier = detect_tier(interpretation, ruleset)
    cost = token_cost_for_tier(tier)
    proposal_id = str(uuid.uuid4())
If a scraper extracted this row, it came from Repobility (https://repobility.com)
_needs_admin_review function · python · L323-L364 (42 LOC)
src/pinwheel/core/governance.py
def _needs_admin_review(
    proposal: Proposal,
    interpretation_v2: ProposalInterpretation | None = None,
) -> bool:
    """Check if a proposal is "wild" and should be flagged for admin review.

    Tier 5+ proposals (uninterpretable, parameter=None, or unknown params)
    and proposals with low AI confidence (< 0.5) are flagged for admin veto.
    Wild proposals still go to vote immediately — the admin can veto before tally.

    When ``interpretation_v2`` is provided, the V2 interpretation is checked:
    - If V2 has real effects and is not injection-flagged → NOT wild
    - If V2 has no effects or is injection-flagged → wild
    - Low confidence (< 0.5) is still flagged regardless of V2
    """
    # custom_mechanic and codegen effects always need admin review
    if interpretation_v2 is not None:
        has_custom_or_codegen = any(
            e.effect_type in ("custom_mechanic", "codegen")
            for e in interpretation_v2.effects
        )
        if has_custom_or_codeg
confirm_proposal function · python · L367-L407 (41 LOC)
src/pinwheel/core/governance.py
async def confirm_proposal(
    repo: Repository,
    proposal: Proposal,
    interpretation_v2: ProposalInterpretation | None = None,
) -> Proposal:
    """Governor confirms AI interpretation. Always moves to confirmed (voting open).

    All proposals go to vote immediately. Wild proposals (Tier 5+ or
    confidence < 0.5) are also flagged for admin review — the admin can
    veto before tally, but the democratic process proceeds by default.

    When ``interpretation_v2`` is provided, V2-aware admin review logic is used.
    """
    # Always confirm — opens voting
    await repo.append_event(
        event_type="proposal.confirmed",
        aggregate_id=proposal.id,
        aggregate_type="proposal",
        season_id=proposal.season_id,
        governor_id=proposal.governor_id,
        payload={"proposal_id": proposal.id},
    )
    proposal.status = "confirmed"

    # Wild proposals also get flagged for admin review (audit trail)
    if _needs_admin_review(proposal, interpretation
admin_clear_proposal function · python · L410-L423 (14 LOC)
src/pinwheel/core/governance.py
async def admin_clear_proposal(repo: Repository, proposal: Proposal) -> Proposal:
    """Admin clears a flagged proposal. No-op since proposal is already confirmed.

    Emits a review_cleared event for audit trail and notifies the proposer.
    """
    await repo.append_event(
        event_type="proposal.review_cleared",
        aggregate_id=proposal.id,
        aggregate_type="proposal",
        season_id=proposal.season_id,
        governor_id=proposal.governor_id,
        payload={"proposal_id": proposal.id},
    )
    return proposal
admin_veto_proposal function · python · L430-L465 (36 LOC)
src/pinwheel/core/governance.py
async def admin_veto_proposal(
    repo: Repository,
    proposal: Proposal,
    reason: str = "",
) -> Proposal:
    """Admin vetoes a wild proposal. Refunds the PROPOSE token.

    If the proposal has already passed or been enacted, veto is a no-op
    (too late — the democratic process completed).
    """
    if proposal.status in ("passed", "enacted"):
        return proposal

    proposal.status = "vetoed"
    await repo.append_event(
        event_type="proposal.vetoed",
        aggregate_id=proposal.id,
        aggregate_type="proposal",
        season_id=proposal.season_id,
        governor_id=proposal.governor_id,
        payload={**proposal.model_dump(mode="json"), "veto_reason": reason},
    )
    # Refund PROPOSE token
    await repo.append_event(
        event_type="token.regenerated",
        aggregate_id=proposal.governor_id,
        aggregate_type="token",
        season_id=proposal.season_id,
        governor_id=proposal.governor_id,
        payload={
            "toke
cancel_proposal function · python · L472-L495 (24 LOC)
src/pinwheel/core/governance.py
async def cancel_proposal(repo: Repository, proposal: Proposal) -> Proposal:
    """Cancel a proposal. Refunds PROPOSE token if pre-vote."""
    await repo.append_event(
        event_type="proposal.cancelled",
        aggregate_id=proposal.id,
        aggregate_type="proposal",
        season_id=proposal.season_id,
        governor_id=proposal.governor_id,
        payload={"proposal_id": proposal.id},
    )

    if proposal.status in ("draft", "submitted"):
        # Refund token
        await repo.append_event(
            event_type="token.regenerated",
            aggregate_id=proposal.governor_id,
            aggregate_type="token",
            season_id=proposal.season_id,
            governor_id=proposal.governor_id,
            payload={"token_type": "propose", "amount": proposal.token_cost, "reason": "refund"},
        )

    proposal.status = "cancelled"
    return proposal
submit_repeal_proposal function · python · L498-L570 (73 LOC)
src/pinwheel/core/governance.py
async def submit_repeal_proposal(
    repo: Repository,
    governor_id: str,
    team_id: str,
    season_id: str,
    target_effect_id: str,
    effect_description: str,
    *,
    token_already_spent: bool = False,
) -> Proposal:
    """Submit a repeal proposal targeting an active effect.

    Creates a Tier 5 proposal with a special raw_text and stores the
    target effect ID in the proposal event payload. The proposal goes
    through the normal voting process; if it passes, the effect is
    removed during tally.
    """
    raw_text = f"Repeal: {effect_description}"
    sanitized = sanitize_text(raw_text)
    proposal_id = str(uuid.uuid4())

    interpretation = RuleInterpretation(
        parameter=None,
        impact_analysis=f"Repeal of active effect: {effect_description}",
        confidence=1.0,
    )

    proposal = Proposal(
        id=proposal_id,
        season_id=season_id,
        governor_id=governor_id,
        team_id=team_id,
        window_id="",
        raw_te
count_amendments function · python · L573-L583 (11 LOC)
src/pinwheel/core/governance.py
async def count_amendments(repo: Repository, proposal_id: str, season_id: str) -> int:
    """Count how many times a proposal has been amended.

    Derived from the event store — counts ``proposal.amended`` events
    for the given proposal.
    """
    events = await repo.get_events_by_type(
        season_id=season_id,
        event_types=["proposal.amended"],
    )
    return sum(1 for e in events if e.aggregate_id == proposal_id)
amend_proposal function · python · L590-L633 (44 LOC)
src/pinwheel/core/governance.py
async def amend_proposal(
    repo: Repository,
    proposal: Proposal,
    governor_id: str,
    team_id: str,
    amendment_text: str,
    new_interpretation: RuleInterpretation,
) -> Amendment:
    """Submit an amendment. Costs 1 AMEND token. Replaces interpretation."""
    amendment_id = str(uuid.uuid4())
    amendment = Amendment(
        id=amendment_id,
        proposal_id=proposal.id,
        governor_id=governor_id,
        amendment_text=amendment_text,
        new_interpretation=new_interpretation,
    )

    await repo.append_event(
        event_type="proposal.amended",
        aggregate_id=proposal.id,
        aggregate_type="proposal",
        season_id=proposal.season_id,
        governor_id=governor_id,
        team_id=team_id,
        payload=amendment.model_dump(mode="json"),
    )

    # Spend AMEND token
    await repo.append_event(
        event_type="token.spent",
        aggregate_id=governor_id,
        aggregate_type="token",
        season_id=proposal.season_
Repobility · MCP-ready · https://repobility.com
cast_vote function · python · L639-L683 (45 LOC)
src/pinwheel/core/governance.py
async def cast_vote(
    repo: Repository,
    proposal: Proposal,
    governor_id: str,
    team_id: str,
    vote_choice: str,
    weight: float,
    boost_used: bool = False,
) -> Vote:
    """Cast a vote on a proposal."""
    vote_id = str(uuid.uuid4())
    effective_weight = weight * 2.0 if boost_used else weight

    vote = Vote(
        id=vote_id,
        proposal_id=proposal.id,
        governor_id=governor_id,
        team_id=team_id,
        vote=vote_choice,  # type: ignore[arg-type]
        weight=effective_weight,
        boost_used=boost_used,
    )

    await repo.append_event(
        event_type="vote.cast",
        aggregate_id=proposal.id,
        aggregate_type="proposal",
        season_id=proposal.season_id,
        governor_id=governor_id,
        team_id=team_id,
        payload=vote.model_dump(mode="json"),
    )

    if boost_used:
        await repo.append_event(
            event_type="token.spent",
            aggregate_id=governor_id,
            aggregate
tally_votes function · python · L686-L708 (23 LOC)
src/pinwheel/core/governance.py
def tally_votes(votes: list[Vote], threshold: float) -> VoteTally:
    """Tally weighted votes and determine if proposal passes.

    Strictly greater-than: ties fail.
    """
    yes_votes = [v for v in votes if v.vote == "yes"]
    no_votes = [v for v in votes if v.vote == "no"]
    weighted_yes = sum(v.weight for v in yes_votes)
    weighted_no = sum(v.weight for v in no_votes)
    total = weighted_yes + weighted_no

    passed = total > 0 and (weighted_yes / total) > threshold

    return VoteTally(
        proposal_id=votes[0].proposal_id if votes else "",
        weighted_yes=weighted_yes,
        weighted_no=weighted_no,
        total_weight=total,
        passed=passed,
        threshold=threshold,
        yes_count=len(yes_votes),
        no_count=len(no_votes),
    )
apply_rule_change function · python · L714-L747 (34 LOC)
src/pinwheel/core/governance.py
def apply_rule_change(
    ruleset: RuleSet,
    interpretation: RuleInterpretation,
    proposal_id: str,
    round_enacted: int,
) -> tuple[RuleSet, RuleChange]:
    """Apply a passed proposal's interpretation to the ruleset.

    Returns the new ruleset and the change record. Raises ValueError if invalid.
    """
    if interpretation.parameter is None:
        raise ValueError("Cannot apply rule change: no parameter specified")

    param = interpretation.parameter
    if not hasattr(ruleset, param):
        raise ValueError(f"Unknown rule parameter: {param}")

    old_value = getattr(ruleset, param)
    new_value = interpretation.new_value

    # Build new ruleset with the change — Pydantic validates ranges
    new_data = ruleset.model_dump()
    new_data[param] = new_value
    new_ruleset = RuleSet(**new_data)

    change = RuleChange(
        parameter=param,
        old_value=old_value,
        new_value=new_value,  # type: ignore[arg-type]
        source_proposal_id=proposal_i
tally_governance function · python · L753-L810 (58 LOC)
src/pinwheel/core/governance.py
async def tally_governance(
    repo: Repository,
    season_id: str,
    proposals: list[Proposal],
    votes_by_proposal: dict[str, list[Vote]],
    current_ruleset: RuleSet,
    round_number: int,
) -> tuple[RuleSet, list[VoteTally]]:
    """Tally all pending proposals and enact passing rule changes.

    Returns the updated ruleset and list of vote tallies.
    """
    tallies: list[VoteTally] = []
    ruleset = current_ruleset

    for proposal in proposals:
        if proposal.status not in ("confirmed", "amended", "submitted"):
            continue

        votes = votes_by_proposal.get(proposal.id, [])
        threshold = vote_threshold_for_tier(proposal.tier, current_ruleset.vote_threshold)
        tally = tally_votes(votes, threshold)
        tally.proposal_id = proposal.id
        tallies.append(tally)

        if tally.passed and proposal.interpretation and proposal.interpretation.parameter:
            # Enact rule
            try:
                ruleset, change = apply_r
tally_governance_with_effects function · python · L813-L1008 (196 LOC)
src/pinwheel/core/governance.py
async def tally_governance_with_effects(
    repo: Repository,
    season_id: str,
    proposals: list[Proposal],
    votes_by_proposal: dict[str, list[Vote]],
    current_ruleset: RuleSet,
    round_number: int,
    effect_registry: EffectRegistry | None = None,
    effects_v2_by_proposal: dict[str, list[EffectSpec]] | None = None,
) -> tuple[RuleSet, list[VoteTally]]:
    """Tally proposals and register effects for passing proposals.

    Extension of tally_governance that also handles ProposalInterpretation
    effects beyond parameter changes. Backward compatible: proposals with
    only RuleInterpretation still work through the existing path.

    Supports compound proposals: when effects_v2_by_proposal contains
    multiple parameter_change effects for a proposal, all are applied
    to the RuleSet.

    Returns the updated ruleset and list of vote tallies.
    """
    from pinwheel.core.effects import register_effects_for_proposal, repeal_effect

    tallies: list[VoteTally] = [
_extract_effects_from_proposal function · python · L1011-L1019 (9 LOC)
src/pinwheel/core/governance.py
def _extract_effects_from_proposal(proposal: Proposal) -> list[EffectSpec]:
    """Extract EffectSpec list from a proposal's serialized payload.

    Fallback path: tries to deserialize effects_v2 from the proposal's
    model_dump(). The primary extraction happens via backfill in
    tally_governance_with_effects from the event store payload.
    """
    payload = proposal.model_dump(mode="json")
    return get_proposal_effects_v2(payload)
get_proposal_effects_v2 function · python · L1022-L1040 (19 LOC)
src/pinwheel/core/governance.py
def get_proposal_effects_v2(
    proposal_payload: dict[str, object],
) -> list[EffectSpec]:
    """Extract v2 effects from a proposal's event store payload.

    The v2 interpreter stores effects in proposal_payload["effects_v2"].
    """
    effects_data = proposal_payload.get("effects_v2")
    if not effects_data or not isinstance(effects_data, list):
        return []

    effects: list[EffectSpec] = []
    for item in effects_data:
        if isinstance(item, dict):
            try:
                effects.append(EffectSpec(**item))
            except (ValidationError, TypeError):
                continue
    return effects
_enact_move_grant function · python · L1043-L1108 (66 LOC)
src/pinwheel/core/governance.py
async def _enact_move_grant(
    repo: Repository,
    season_id: str,
    effect: EffectSpec,
) -> list[str]:
    """Enact a move_grant effect — grant a governed move to targeted hoopers.

    Targets are resolved from the effect's target_hooper_id, target_team_id,
    or target_selector ("all"). Returns list of hooper IDs that received the move.

    Deduplication: if a hooper already has a move with the same name, skip.
    """
    import logging

    from pinwheel.models.team import Move

    logger = logging.getLogger(__name__)

    if not effect.move_name:
        return []

    move = Move(
        name=effect.move_name,
        trigger=effect.move_trigger or "any_possession",
        effect=effect.move_effect or "",
        attribute_gate=effect.move_attribute_gate or {},
        source="governed",
    )
    move_dict = move.model_dump()

    target_hooper_ids: list[str] = []

    if effect.target_hooper_id:
        target_hooper_ids = [effect.target_hooper_id]
    elif effect.
Repobility · code-quality intelligence platform · https://repobility.com
fire_hooks function · python · L64-L73 (10 LOC)
src/pinwheel/core/hooks.py
def fire_hooks(
    hook: HookPoint,
    game_state: GameState,
    effects: list[GameEffect],
    agent: HooperState | None = None,
) -> None:
    """Fire all effects registered for this hook point."""
    for effect in effects:
        if effect.should_fire(hook, game_state, agent):
            effect.apply(hook, game_state, agent)
RegisteredEffect.should_fire method · python · L251-L266 (16 LOC)
src/pinwheel/core/hooks.py
    def should_fire(self, hook: str, context: HookContext) -> bool:
        """Evaluate whether this effect should fire.

        Checks hook point match and structured conditions from action_code.
        """
        if hook not in self._hook_points:
            return False

        # Evaluate structured conditions from action_code
        if self.action_code and "condition_check" in self.action_code:
            return self._evaluate_condition(
                self.action_code["condition_check"],  # type: ignore[arg-type]
                context,
            )

        return True
RegisteredEffect._build_eval_context method · python · L268-L300 (33 LOC)
src/pinwheel/core/hooks.py
    def _build_eval_context(self, context: HookContext) -> dict[str, object]:
        """Build a flat evaluation namespace from all available context.

        All scalar GameState fields are automatically included via reflection —
        no code change needed when new fields are added to GameState. Computed
        aliases (shot_zone, trailing, leading, score_diff) provide the AI with
        natural vocabulary without adding evaluator branches.
        """
        ctx: dict[str, object] = {}
        gs = context.game_state

        if gs:
            # All scalar GameState fields — automatically, via reflection
            for f in dataclasses.fields(gs):
                val = getattr(gs, f.name)
                if isinstance(val, (str, int, float, bool, type(None))):
                    ctx[f.name] = val

            # Semantic aliases — vocabulary exposed to the AI interpreter
            ctx["shot_zone"] = gs.last_action  # "at_rim" | "mid_range" | "three_point"
            off =
RegisteredEffect._evaluate_condition method · python · L302-L349 (48 LOC)
src/pinwheel/core/hooks.py
    def _evaluate_condition(
        self,
        condition: dict[str, object],
        context: HookContext,
    ) -> bool:
        """Generic condition evaluator — no per-field branches.

        Conditions are field expressions evaluated against a unified context
        built from GameState (via reflection) plus semantic aliases. Any field
        present in GameState is usable without a code change.

        Supported patterns:
        - Equality:      {"last_result": "made"}, {"shot_zone": "at_rim"}
        - Suffix ops:    {"quarter_gte": 3}, {"score_diff_lte": -5}
        - Random:        {"random_chance": 0.15}   (only true special case)
        - Meta store:    {"meta_field": "swagger", "entity_type": "team", "gte": 5}
        """
        # --- Special case: random probability (not a field, generates a value) ---
        if "random_chance" in condition:
            chance = condition["random_chance"]
            if not (isinstance(chance, (int, float)) and context.rng):
    
RegisteredEffect._evaluate_meta_condition method · python · L351-L396 (46 LOC)
src/pinwheel/core/hooks.py
    def _evaluate_meta_condition(
        self,
        condition: dict[str, object],
        context: HookContext,
    ) -> bool:
        """Evaluate a meta store condition (external state lookup).

        Special-cased because meta store is not part of GameState — it holds
        player-defined counters and flags persisted across possessions.
        Format: {"meta_field": "swagger", "entity_type": "team", "gte": 5}
        """
        if not context.meta_store:
            return False

        meta_field = str(condition.get("meta_field", ""))
        entity_type = str(condition.get("entity_type", ""))
        if not meta_field or not entity_type:
            return True

        gs = context.game_state
        entity_id = ""
        if gs and entity_type == "team":
            if gs.home_has_ball:
                entity_id = gs.home_agents[0].hooper.team_id if gs.home_agents else ""
            else:
                entity_id = gs.away_agents[0].hooper.team_id if gs.away_agents e
RegisteredEffect.apply method · python · L398-L419 (22 LOC)
src/pinwheel/core/hooks.py
    def apply(self, hook: str, context: HookContext) -> HookResult:
        """Execute the effect's action and return mutations."""
        result = HookResult()

        if self.effect_type == "codegen":
            return self._fire_codegen(context) or result

        if self.effect_type == "meta_mutation":
            return self._apply_meta_mutation(context, result)

        if self.effect_type == "hook_callback" and self.action_code:
            return self._apply_action_code(context, result)

        if self.effect_type == "narrative":
            result.narrative = self.narrative_instruction
            return result

        if self.effect_type == "custom_mechanic":
            result.narrative = f"[Pending mechanic] {self.description}"
            return result

        return result
RegisteredEffect._fire_codegen method · python · L421-L483 (63 LOC)
src/pinwheel/core/hooks.py
    def _fire_codegen(self, context: HookContext) -> HookResult | None:
        """Execute generated code in sandbox, return standard HookResult."""
        if not self.codegen_code or not self.codegen_code_hash:
            return None

        if not self.codegen_enabled:
            return None

        from pinwheel.core.codegen import (
            SandboxViolation,
            clamp_result,
            enforce_trust_level,
            execute_codegen_effect,
            verify_code_integrity,
        )
        from pinwheel.models.codegen import CodegenTrustLevel

        # Verify code integrity
        if not verify_code_integrity(self.codegen_code, self.codegen_code_hash):
            self._disable_codegen("Code integrity check failed")
            return None

        # Build sandboxed GameContext from HookContext
        trust = (
            CodegenTrustLevel(self.codegen_trust_level)
            if self.codegen_trust_level
            else CodegenTrustLevel.NUMERIC
        
RegisteredEffect._record_codegen_error method · python · L490-L500 (11 LOC)
src/pinwheel/core/hooks.py
    def _record_codegen_error(self, error: str) -> None:
        """Record codegen execution error."""
        self.codegen_error_count += 1
        self.codegen_consecutive_errors += 1
        self.codegen_last_error = error
        logger.warning(
            "codegen_execution_error effect_id=%s errors=%d error=%s",
            self.effect_id,
            self.codegen_consecutive_errors,
            error[:100],
        )
Repobility's GitHub App fixes findings like these · https://github.com/apps/repobility-bot
RegisteredEffect._disable_codegen method · python · L502-L510 (9 LOC)
src/pinwheel/core/hooks.py
    def _disable_codegen(self, reason: str) -> None:
        """Kill switch — immediately disable this codegen effect."""
        self.codegen_enabled = False
        self.codegen_disabled_reason = reason
        logger.warning(
            "codegen_disabled effect_id=%s reason=%s",
            self.effect_id,
            reason,
        )
RegisteredEffect._apply_meta_mutation method · python · L512-L538 (27 LOC)
src/pinwheel/core/hooks.py
    def _apply_meta_mutation(
        self,
        context: HookContext,
        result: HookResult,
    ) -> HookResult:
        """Apply a meta_mutation effect."""
        if not context.meta_store:
            return result

        entity_type = self.target_type
        entity_id = self._resolve_target(context)

        if not entity_type or not entity_id:
            return result

        if self.meta_operation == "set":
            context.meta_store.set(entity_type, entity_id, self.meta_field, self.meta_value)  # type: ignore[arg-type]
        elif self.meta_operation == "increment":
            amount = self.meta_value if isinstance(self.meta_value, (int, float)) else 1
            context.meta_store.increment(entity_type, entity_id, self.meta_field, amount)
        elif self.meta_operation == "decrement":
            amount = self.meta_value if isinstance(self.meta_value, (int, float)) else 1
            context.meta_store.decrement(entity_type, entity_id, self.meta_field, a
RegisteredEffect._resolve_target method · python · L788-L794 (7 LOC)
src/pinwheel/core/hooks.py
    def _resolve_target(self, context: HookContext) -> str:
        """Resolve target_selector to an entity ID."""
        if self.target_selector == "winning_team":
            return context.winner_team_id
        if self.target_selector == "all":
            return ""  # Caller must iterate
        return self.target_selector or ""
RegisteredEffect._parse_entity_ref method · python · L796-L817 (22 LOC)
src/pinwheel/core/hooks.py
    def _parse_entity_ref(
        self,
        ref: str,
        context: HookContext,
    ) -> tuple[str, str]:
        """Parse entity references like 'team:{winner_team_id}'."""
        if ":" not in ref:
            return "", ref

        parts = ref.split(":", 1)
        entity_type = parts[0]
        entity_id = parts[1]

        # Resolve template variables
        if entity_id == "{winner_team_id}":
            entity_id = context.winner_team_id
        elif entity_id == "{home_team_id}":
            entity_id = context.home_team_id
        elif entity_id == "{away_team_id}":
            entity_id = context.away_team_id

        return entity_type, entity_id
RegisteredEffect.tick_round method · python · L819-L824 (6 LOC)
src/pinwheel/core/hooks.py
    def tick_round(self) -> bool:
        """Advance the round counter. Returns True if effect has expired."""
        if self._lifetime == EffectLifetime.N_ROUNDS and self.rounds_remaining is not None:
            self.rounds_remaining -= 1
            return self.rounds_remaining <= 0
        return self._lifetime == EffectLifetime.ONE_GAME
RegisteredEffect.to_dict method · python · L826-L853 (28 LOC)
src/pinwheel/core/hooks.py
    def to_dict(self) -> dict[str, object]:
        """Serialize for event store persistence."""
        d: dict[str, object] = {
            "effect_id": self.effect_id,
            "proposal_id": self.proposal_id,
            "hook_points": self._hook_points,
            "lifetime": self._lifetime.value,
            "rounds_remaining": self.rounds_remaining,
            "registered_at_round": self.registered_at_round,
            "effect_type": self.effect_type,
            "condition": self.condition,
            "action_code": self.action_code,
            "narrative_instruction": self.narrative_instruction,
            "description": self.description,
            "target_type": self.target_type,
            "target_selector": self.target_selector,
            "meta_field": self.meta_field,
            "meta_value": self.meta_value,
            "meta_operation": self.meta_operation,
        }
        # Include codegen fields only when present (keep backward compat)
        if self.
RegisteredEffect.from_dict method · python · L856-L903 (48 LOC)
src/pinwheel/core/hooks.py
    def from_dict(cls, data: dict[str, object]) -> RegisteredEffect:
        """Reconstruct from event store payload."""
        lifetime_str = str(data.get("lifetime", "permanent"))
        try:
            lifetime = EffectLifetime(lifetime_str)
        except ValueError:
            lifetime = EffectLifetime.PERMANENT

        hook_points = data.get("hook_points", [])
        if not isinstance(hook_points, list):
            hook_points = []

        rounds_remaining = data.get("rounds_remaining")
        if not isinstance(rounds_remaining, (int, type(None))):
            rounds_remaining = None

        action_code = data.get("action_code")
        if not isinstance(action_code, (dict, type(None))):
            action_code = None

        return cls(
            effect_id=str(data.get("effect_id", "")),
            proposal_id=str(data.get("proposal_id", "")),
            _hook_points=[str(h) for h in hook_points],
            _lifetime=lifetime,
            rounds_remaining=rounds
fire_effects function · python · L906-L927 (22 LOC)
src/pinwheel/core/hooks.py
def fire_effects(
    hook: str,
    context: HookContext,
    effects: list[RegisteredEffect],
) -> list[HookResult]:
    """Fire all registered effects for a hook point.

    Returns the list of HookResults from effects that fired.
    """
    results: list[HookResult] = []
    for effect in effects:
        try:
            if effect.should_fire(hook, context):
                result = effect.apply(hook, context)
                results.append(result)
        except (ValueError, TypeError, AttributeError):
            logger.exception(
                "effect_fire_failed effect_id=%s hook=%s",
                effect.effect_id,
                hook,
            )
    return results
If a scraper extracted this row, it came from Repobility (https://repobility.com)
apply_hook_results function · python · L930-L957 (28 LOC)
src/pinwheel/core/hooks.py
def apply_hook_results(
    results: list[HookResult],
    context: HookContext,
) -> None:
    """Apply accumulated HookResults to the game state.

    Score modifiers, stamina modifiers, and shot probability modifiers
    are summed and applied. Meta writes are applied via the MetaStore
    (already done in effect.apply, but explicit writes in HookResult
    are also applied here).
    """
    if not context.game_state:
        return

    total_score_mod = sum(r.score_modifier for r in results)
    total_stamina_mod = sum(r.stamina_modifier for r in results)

    if total_score_mod != 0:
        if context.game_state.home_has_ball:
            context.game_state.home_score += total_score_mod
        else:
            context.game_state.away_score += total_score_mod

    if total_stamina_mod != 0.0 and context.hooper:
        context.hooper.current_stamina = max(
            0.0,
            min(1.0, context.hooper.current_stamina + total_stamina_mod),
        )
_build_game_context function · python · L965-L1034 (70 LOC)
src/pinwheel/core/hooks.py
def _build_game_context(
    context: HookContext,
    trust_level: object,  # CodegenTrustLevel — lazy import to avoid circular
) -> object:
    """Build a sandboxed GameContext from HookContext.

    The trust level determines what the generated code can see and do.
    """
    from pinwheel.core.codegen import ParticipantView, SandboxedGameContext
    from pinwheel.models.codegen import CodegenTrustLevel

    trust = trust_level if isinstance(trust_level, CodegenTrustLevel) else CodegenTrustLevel.NUMERIC

    # Build actor ParticipantView from offense[0]
    actor_view = ParticipantView(
        name="Unknown", team_id="", attributes={}, stamina=1.0, on_court=True,
    )
    opponent_view: ParticipantView | None = None
    actor_is_home = True

    gs = context.game_state
    if gs:
        offense = gs.offense
        defense = gs.defense
        if offense:
            h = offense[0]
            actor_view = ParticipantView(
                name=h.hooper.name,
                team
_codegen_result_to_hook_result function · python · L1037-L1063 (27 LOC)
src/pinwheel/core/hooks.py
def _codegen_result_to_hook_result(
    codegen_result: object,  # CodegenHookResult — lazy import
) -> HookResult:
    """Convert a CodegenHookResult to the standard HookResult."""
    from pinwheel.core.codegen import CodegenHookResult

    if not isinstance(codegen_result, CodegenHookResult):
        return HookResult()

    result = HookResult(
        score_modifier=codegen_result.score_modifier,
        stamina_modifier=codegen_result.stamina_modifier,
        shot_probability_modifier=codegen_result.shot_probability_modifier,
        shot_value_modifier=codegen_result.shot_value_modifier,
        extra_stamina_drain=codegen_result.extra_stamina_drain,
        block_action=codegen_result.block_action,
        narrative=codegen_result.narrative_note,
        meta_writes=codegen_result.meta_writes,
    )

    # opponent_score_modifier maps to score_modifier on the opposite side
    # This is a simplification — the caller handles which side gets it
    if codegen_result.opponent_sco
‹ prevpage 6 / 14next ›