Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| abb1c71f0a | |||
|
|
bf3a8ca0d5 | ||
|
|
bbb5689b1f | ||
|
|
0d009eb1f8 |
31
.githooks/install-hooks.sh
Executable file
31
.githooks/install-hooks.sh
Executable file
@ -0,0 +1,31 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# Install git hooks for this repository
|
||||||
|
#
|
||||||
|
|
||||||
|
REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
|
||||||
|
|
||||||
|
if [ -z "$REPO_ROOT" ]; then
|
||||||
|
echo "Error: Not in a git repository"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
HOOKS_DIR="$REPO_ROOT/.githooks"
|
||||||
|
GIT_HOOKS_DIR="$REPO_ROOT/.git/hooks"
|
||||||
|
|
||||||
|
echo "Installing git hooks..."
|
||||||
|
|
||||||
|
if [ -f "$HOOKS_DIR/pre-commit" ]; then
|
||||||
|
cp "$HOOKS_DIR/pre-commit" "$GIT_HOOKS_DIR/pre-commit"
|
||||||
|
chmod +x "$GIT_HOOKS_DIR/pre-commit"
|
||||||
|
echo "Installed pre-commit hook"
|
||||||
|
else
|
||||||
|
echo "pre-commit hook not found in $HOOKS_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "The pre-commit hook will:"
|
||||||
|
echo " - Auto-fix ruff lint violations (unused imports, formatting, etc.)"
|
||||||
|
echo " - Block commits only on truly unfixable issues"
|
||||||
|
echo ""
|
||||||
|
echo "To bypass in emergency: git commit --no-verify"
|
||||||
53
.githooks/pre-commit
Executable file
53
.githooks/pre-commit
Executable file
@ -0,0 +1,53 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# Pre-commit hook: ruff lint check on staged Python files.
|
||||||
|
# Catches syntax errors, unused imports, and basic issues before commit.
|
||||||
|
# To bypass in emergency: git commit --no-verify
|
||||||
|
#
|
||||||
|
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
REPO_ROOT=$(git rev-parse --show-toplevel)
|
||||||
|
cd "$REPO_ROOT"
|
||||||
|
|
||||||
|
STAGED_PY=$(git diff --cached --name-only --diff-filter=ACM -z -- '*.py')
|
||||||
|
if [ -z "$STAGED_PY" ]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "ruff check on staged files..."
|
||||||
|
|
||||||
|
# Stash unstaged changes so ruff only operates on staged content.
|
||||||
|
# Without this, ruff --fix runs on the full working tree file (staged +
|
||||||
|
# unstaged), and the subsequent git add would silently include unstaged
|
||||||
|
# changes in the commit — breaking git add -p workflows.
|
||||||
|
STASHED=0
|
||||||
|
if git stash --keep-index -q 2>/dev/null; then
|
||||||
|
STASHED=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Auto-fix what we can, then re-stage the fixed files
|
||||||
|
printf '%s' "$STAGED_PY" | xargs -0 ruff check --fix --exit-zero
|
||||||
|
printf '%s' "$STAGED_PY" | xargs -0 git add
|
||||||
|
|
||||||
|
# Restore unstaged changes
|
||||||
|
if [ $STASHED -eq 1 ]; then
|
||||||
|
git stash pop -q
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Now check for remaining unfixable issues
|
||||||
|
printf '%s' "$STAGED_PY" | xargs -0 ruff check
|
||||||
|
RUFF_EXIT=$?
|
||||||
|
|
||||||
|
if [ $RUFF_EXIT -ne 0 ]; then
|
||||||
|
echo ""
|
||||||
|
echo -e "${RED}Pre-commit checks failed (unfixable issues). Commit blocked.${NC}"
|
||||||
|
echo -e "${YELLOW}To bypass (not recommended): git commit --no-verify${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${GREEN}All checks passed.${NC}"
|
||||||
|
exit 0
|
||||||
@ -1257,50 +1257,15 @@ refractor_card_state_team_index = ModelIndex(
|
|||||||
RefractorCardState.add_index(refractor_card_state_team_index)
|
RefractorCardState.add_index(refractor_card_state_team_index)
|
||||||
|
|
||||||
|
|
||||||
class RefractorTierBoost(BaseModel):
|
|
||||||
track = ForeignKeyField(RefractorTrack)
|
|
||||||
tier = IntegerField() # 1-4
|
|
||||||
boost_type = CharField() # e.g. 'rating', 'stat'
|
|
||||||
boost_target = CharField() # e.g. 'contact_vl', 'power_vr'
|
|
||||||
boost_value = FloatField(default=0.0)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
database = db
|
|
||||||
table_name = "refractor_tier_boost"
|
|
||||||
|
|
||||||
|
|
||||||
refractor_tier_boost_index = ModelIndex(
|
|
||||||
RefractorTierBoost,
|
|
||||||
(
|
|
||||||
RefractorTierBoost.track,
|
|
||||||
RefractorTierBoost.tier,
|
|
||||||
RefractorTierBoost.boost_type,
|
|
||||||
RefractorTierBoost.boost_target,
|
|
||||||
),
|
|
||||||
unique=True,
|
|
||||||
)
|
|
||||||
RefractorTierBoost.add_index(refractor_tier_boost_index)
|
|
||||||
|
|
||||||
|
|
||||||
class RefractorCosmetic(BaseModel):
|
|
||||||
name = CharField(unique=True)
|
|
||||||
tier_required = IntegerField(default=0)
|
|
||||||
cosmetic_type = CharField() # 'frame', 'badge', 'theme'
|
|
||||||
css_class = CharField(null=True)
|
|
||||||
asset_url = CharField(null=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
database = db
|
|
||||||
table_name = "refractor_cosmetic"
|
|
||||||
|
|
||||||
|
|
||||||
class RefractorBoostAudit(BaseModel):
|
class RefractorBoostAudit(BaseModel):
|
||||||
card_state = ForeignKeyField(RefractorCardState, on_delete="CASCADE")
|
card_state = ForeignKeyField(RefractorCardState, on_delete="CASCADE")
|
||||||
tier = IntegerField() # 1-4
|
tier = IntegerField() # 1-4
|
||||||
battingcard = ForeignKeyField(BattingCard, null=True)
|
battingcard = ForeignKeyField(BattingCard, null=True)
|
||||||
pitchingcard = ForeignKeyField(PitchingCard, null=True)
|
pitchingcard = ForeignKeyField(PitchingCard, null=True)
|
||||||
variant_created = IntegerField()
|
variant_created = IntegerField()
|
||||||
boost_delta_json = TextField() # JSONB in PostgreSQL; TextField for SQLite test compat
|
boost_delta_json = (
|
||||||
|
TextField()
|
||||||
|
) # JSONB in PostgreSQL; TextField for SQLite test compat
|
||||||
applied_at = DateTimeField(default=datetime.now)
|
applied_at = DateTimeField(default=datetime.now)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -1313,8 +1278,6 @@ if not SKIP_TABLE_CREATION:
|
|||||||
[
|
[
|
||||||
RefractorTrack,
|
RefractorTrack,
|
||||||
RefractorCardState,
|
RefractorCardState,
|
||||||
RefractorTierBoost,
|
|
||||||
RefractorCosmetic,
|
|
||||||
RefractorBoostAudit,
|
RefractorBoostAudit,
|
||||||
],
|
],
|
||||||
safe=True,
|
safe=True,
|
||||||
|
|||||||
7
migrations/2026-04-05_drop_unused_refractor_tables.sql
Normal file
7
migrations/2026-04-05_drop_unused_refractor_tables.sql
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
-- Drop orphaned RefractorTierBoost and RefractorCosmetic tables.
|
||||||
|
-- These were speculative schema from the initial Refractor design that were
|
||||||
|
-- never used — boosts are hardcoded in refractor_boost.py and tier visuals
|
||||||
|
-- are embedded in CSS templates. Both tables have zero rows on dev and prod.
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS refractor_tier_boost;
|
||||||
|
DROP TABLE IF EXISTS refractor_cosmetic;
|
||||||
@ -48,8 +48,6 @@ from app.db_engine import (
|
|||||||
PitchingCard,
|
PitchingCard,
|
||||||
RefractorTrack,
|
RefractorTrack,
|
||||||
RefractorCardState,
|
RefractorCardState,
|
||||||
RefractorTierBoost,
|
|
||||||
RefractorCosmetic,
|
|
||||||
RefractorBoostAudit,
|
RefractorBoostAudit,
|
||||||
ScoutOpportunity,
|
ScoutOpportunity,
|
||||||
ScoutClaim,
|
ScoutClaim,
|
||||||
@ -81,8 +79,6 @@ _TEST_MODELS = [
|
|||||||
ScoutClaim,
|
ScoutClaim,
|
||||||
RefractorTrack,
|
RefractorTrack,
|
||||||
RefractorCardState,
|
RefractorCardState,
|
||||||
RefractorTierBoost,
|
|
||||||
RefractorCosmetic,
|
|
||||||
BattingCard,
|
BattingCard,
|
||||||
PitchingCard,
|
PitchingCard,
|
||||||
RefractorBoostAudit,
|
RefractorBoostAudit,
|
||||||
|
|||||||
@ -72,8 +72,6 @@ from app.db_engine import (
|
|||||||
Rarity,
|
Rarity,
|
||||||
RefractorBoostAudit,
|
RefractorBoostAudit,
|
||||||
RefractorCardState,
|
RefractorCardState,
|
||||||
RefractorCosmetic,
|
|
||||||
RefractorTierBoost,
|
|
||||||
RefractorTrack,
|
RefractorTrack,
|
||||||
Roster,
|
Roster,
|
||||||
RosterSlot,
|
RosterSlot,
|
||||||
@ -123,8 +121,6 @@ _WP13_MODELS = [
|
|||||||
PitchingCardRatings,
|
PitchingCardRatings,
|
||||||
RefractorTrack,
|
RefractorTrack,
|
||||||
RefractorCardState,
|
RefractorCardState,
|
||||||
RefractorTierBoost,
|
|
||||||
RefractorCosmetic,
|
|
||||||
RefractorBoostAudit,
|
RefractorBoostAudit,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@ -52,8 +52,6 @@ from app.db_engine import (
|
|||||||
Rarity,
|
Rarity,
|
||||||
RefractorBoostAudit,
|
RefractorBoostAudit,
|
||||||
RefractorCardState,
|
RefractorCardState,
|
||||||
RefractorCosmetic,
|
|
||||||
RefractorTierBoost,
|
|
||||||
RefractorTrack,
|
RefractorTrack,
|
||||||
Roster,
|
Roster,
|
||||||
RosterSlot,
|
RosterSlot,
|
||||||
@ -111,8 +109,6 @@ _BOOST_INT_MODELS = [
|
|||||||
PitchingCardRatings,
|
PitchingCardRatings,
|
||||||
RefractorTrack,
|
RefractorTrack,
|
||||||
RefractorCardState,
|
RefractorCardState,
|
||||||
RefractorTierBoost,
|
|
||||||
RefractorCosmetic,
|
|
||||||
RefractorBoostAudit,
|
RefractorBoostAudit,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@ -5,8 +5,6 @@ Covers WP-01 acceptance criteria:
|
|||||||
- RefractorTrack: CRUD and unique-name constraint
|
- RefractorTrack: CRUD and unique-name constraint
|
||||||
- RefractorCardState: CRUD, defaults, unique-(player,team) constraint,
|
- RefractorCardState: CRUD, defaults, unique-(player,team) constraint,
|
||||||
and FK resolution back to RefractorTrack
|
and FK resolution back to RefractorTrack
|
||||||
- RefractorTierBoost: CRUD and unique-(track, tier, boost_type, boost_target)
|
|
||||||
- RefractorCosmetic: CRUD and unique-name constraint
|
|
||||||
- BattingSeasonStats: CRUD with defaults, unique-(player, team, season),
|
- BattingSeasonStats: CRUD with defaults, unique-(player, team, season),
|
||||||
and in-place stat accumulation
|
and in-place stat accumulation
|
||||||
|
|
||||||
@ -22,8 +20,6 @@ from playhouse.shortcuts import model_to_dict
|
|||||||
from app.db_engine import (
|
from app.db_engine import (
|
||||||
BattingSeasonStats,
|
BattingSeasonStats,
|
||||||
RefractorCardState,
|
RefractorCardState,
|
||||||
RefractorCosmetic,
|
|
||||||
RefractorTierBoost,
|
|
||||||
RefractorTrack,
|
RefractorTrack,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -134,115 +130,6 @@ class TestRefractorCardState:
|
|||||||
assert resolved_track.name == "Batter Track"
|
assert resolved_track.name == "Batter Track"
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# RefractorTierBoost
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
class TestRefractorTierBoost:
|
|
||||||
"""Tests for RefractorTierBoost, the per-tier stat/rating bonus table.
|
|
||||||
|
|
||||||
Each row maps a (track, tier) combination to a single boost — the
|
|
||||||
specific stat or rating column to buff and by how much. The four-
|
|
||||||
column unique constraint prevents double-booking the same boost slot.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def test_create_tier_boost(self, track):
|
|
||||||
"""Creating a boost row persists all fields accurately.
|
|
||||||
|
|
||||||
Verifies boost_type, boost_target, and boost_value are stored
|
|
||||||
and retrieved without modification.
|
|
||||||
"""
|
|
||||||
boost = RefractorTierBoost.create(
|
|
||||||
track=track,
|
|
||||||
tier=1,
|
|
||||||
boost_type="rating",
|
|
||||||
boost_target="contact_vl",
|
|
||||||
boost_value=1.5,
|
|
||||||
)
|
|
||||||
fetched = RefractorTierBoost.get_by_id(boost.id)
|
|
||||||
assert fetched.track_id == track.id
|
|
||||||
assert fetched.tier == 1
|
|
||||||
assert fetched.boost_type == "rating"
|
|
||||||
assert fetched.boost_target == "contact_vl"
|
|
||||||
assert fetched.boost_value == 1.5
|
|
||||||
|
|
||||||
def test_tier_boost_unique_constraint(self, track):
|
|
||||||
"""Duplicate (track, tier, boost_type, boost_target) raises IntegrityError.
|
|
||||||
|
|
||||||
The four-column unique index ensures that a single boost slot
|
|
||||||
(e.g. Tier-1 contact_vl rating) cannot be defined twice for the
|
|
||||||
same track, which would create ambiguity during evolution evaluation.
|
|
||||||
"""
|
|
||||||
RefractorTierBoost.create(
|
|
||||||
track=track,
|
|
||||||
tier=2,
|
|
||||||
boost_type="rating",
|
|
||||||
boost_target="power_vr",
|
|
||||||
boost_value=2.0,
|
|
||||||
)
|
|
||||||
with pytest.raises(IntegrityError):
|
|
||||||
RefractorTierBoost.create(
|
|
||||||
track=track,
|
|
||||||
tier=2,
|
|
||||||
boost_type="rating",
|
|
||||||
boost_target="power_vr",
|
|
||||||
boost_value=3.0, # different value, same identity columns
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# RefractorCosmetic
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
class TestRefractorCosmetic:
|
|
||||||
"""Tests for RefractorCosmetic, decorative unlocks tied to evolution tiers.
|
|
||||||
|
|
||||||
Cosmetics are purely visual rewards (frames, badges, themes) that a
|
|
||||||
card unlocks when it reaches a required tier. The name column is
|
|
||||||
the stable identifier and carries a UNIQUE constraint.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def test_create_cosmetic(self):
|
|
||||||
"""Creating a cosmetic persists all fields correctly.
|
|
||||||
|
|
||||||
Verifies all columns including optional ones (css_class, asset_url)
|
|
||||||
are stored and retrieved.
|
|
||||||
"""
|
|
||||||
cosmetic = RefractorCosmetic.create(
|
|
||||||
name="Gold Frame",
|
|
||||||
tier_required=2,
|
|
||||||
cosmetic_type="frame",
|
|
||||||
css_class="evo-frame-gold",
|
|
||||||
asset_url="https://cdn.example.com/frames/gold.png",
|
|
||||||
)
|
|
||||||
fetched = RefractorCosmetic.get_by_id(cosmetic.id)
|
|
||||||
assert fetched.name == "Gold Frame"
|
|
||||||
assert fetched.tier_required == 2
|
|
||||||
assert fetched.cosmetic_type == "frame"
|
|
||||||
assert fetched.css_class == "evo-frame-gold"
|
|
||||||
assert fetched.asset_url == "https://cdn.example.com/frames/gold.png"
|
|
||||||
|
|
||||||
def test_cosmetic_unique_name(self):
|
|
||||||
"""Inserting a second cosmetic with the same name raises IntegrityError.
|
|
||||||
|
|
||||||
The UNIQUE constraint on RefractorCosmetic.name prevents duplicate
|
|
||||||
cosmetic definitions that could cause ambiguous tier unlock lookups.
|
|
||||||
"""
|
|
||||||
RefractorCosmetic.create(
|
|
||||||
name="Silver Badge",
|
|
||||||
tier_required=1,
|
|
||||||
cosmetic_type="badge",
|
|
||||||
)
|
|
||||||
with pytest.raises(IntegrityError):
|
|
||||||
RefractorCosmetic.create(
|
|
||||||
name="Silver Badge", # duplicate
|
|
||||||
tier_required=3,
|
|
||||||
cosmetic_type="badge",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# BattingSeasonStats
|
# BattingSeasonStats
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@ -64,8 +64,6 @@ from app.db_engine import (
|
|||||||
ProcessedGame,
|
ProcessedGame,
|
||||||
Rarity,
|
Rarity,
|
||||||
RefractorCardState,
|
RefractorCardState,
|
||||||
RefractorCosmetic,
|
|
||||||
RefractorTierBoost,
|
|
||||||
RefractorTrack,
|
RefractorTrack,
|
||||||
Roster,
|
Roster,
|
||||||
RosterSlot,
|
RosterSlot,
|
||||||
@ -681,8 +679,6 @@ _STATE_API_MODELS = [
|
|||||||
ProcessedGame,
|
ProcessedGame,
|
||||||
RefractorTrack,
|
RefractorTrack,
|
||||||
RefractorCardState,
|
RefractorCardState,
|
||||||
RefractorTierBoost,
|
|
||||||
RefractorCosmetic,
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -41,8 +41,6 @@ from app.db_engine import ( # noqa: E402
|
|||||||
ProcessedGame,
|
ProcessedGame,
|
||||||
Rarity,
|
Rarity,
|
||||||
RefractorCardState,
|
RefractorCardState,
|
||||||
RefractorCosmetic,
|
|
||||||
RefractorTierBoost,
|
|
||||||
RefractorTrack,
|
RefractorTrack,
|
||||||
Roster,
|
Roster,
|
||||||
RosterSlot,
|
RosterSlot,
|
||||||
@ -204,8 +202,6 @@ _TRACK_API_MODELS = [
|
|||||||
ProcessedGame,
|
ProcessedGame,
|
||||||
RefractorTrack,
|
RefractorTrack,
|
||||||
RefractorCardState,
|
RefractorCardState,
|
||||||
RefractorTierBoost,
|
|
||||||
RefractorCosmetic,
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user