Compare commits

..

4 Commits

Author SHA1 Message Date
cal
abb1c71f0a Merge pull request 'chore: pre-commit auto-fixes + remove unused Refractor tables' (#181) from chore/pre-commit-autofix into main
Reviewed-on: #181
2026-04-06 04:08:37 +00:00
Cal Corum
bf3a8ca0d5 chore: remove unused RefractorTierBoost and RefractorCosmetic tables
Speculative schema from initial Refractor design that was 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.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 22:27:26 -05:00
Cal Corum
bbb5689b1f fix: address review feedback (#181)
- pre-commit: guard ruff --fix + git add with git stash --keep-index so
  partial-staging (git add -p) workflows are not silently broken
- pre-commit: use -z / xargs -0 for null-delimited filename handling
- install-hooks.sh: update echo messages to reflect auto-fix behaviour

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 23:03:06 -05:00
Cal Corum
0d009eb1f8 chore: pre-commit hook auto-fixes ruff violations before blocking
Instead of failing and requiring manual fix + re-commit, the hook now
runs ruff check --fix first, re-stages the fixed files, then checks
for remaining unfixable issues.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 22:19:37 -05:00
10 changed files with 94 additions and 173 deletions

31
.githooks/install-hooks.sh Executable file
View 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
View 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

View File

@ -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,

View 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;

View File

@ -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,

View File

@ -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,
] ]

View File

@ -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,
] ]

View File

@ -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
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------

View File

@ -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,
] ]

View File

@ -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,
] ]