Function bodies 688 total
_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 otally_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
winnstep_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 phastep_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_summarRepobility'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 textsanitize_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 3vote_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.75submit_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_codegconfirm_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, interpretationadmin_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 proposaladmin_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={
"tokecancel_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 proposalsubmit_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_tecount_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,
aggregatetally_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_itally_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_rtally_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 TrueRegisteredEffect._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 eRegisteredEffect.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 resultRegisteredEffect._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, aRegisteredEffect._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_idRegisteredEffect.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_GAMERegisteredEffect.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=roundsfire_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 resultsIf 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