diff --git a/.githooks/install-hooks.sh b/.githooks/install-hooks.sh new file mode 100755 index 0000000..61874fd --- /dev/null +++ b/.githooks/install-hooks.sh @@ -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" diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 0000000..a05a3dc --- /dev/null +++ b/.githooks/pre-commit @@ -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 diff --git a/app/db_engine.py b/app/db_engine.py index dcff8ee..29f4c80 100644 --- a/app/db_engine.py +++ b/app/db_engine.py @@ -1257,50 +1257,15 @@ refractor_card_state_team_index = ModelIndex( 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): card_state = ForeignKeyField(RefractorCardState, on_delete="CASCADE") tier = IntegerField() # 1-4 battingcard = ForeignKeyField(BattingCard, null=True) pitchingcard = ForeignKeyField(PitchingCard, null=True) 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) class Meta: @@ -1313,8 +1278,6 @@ if not SKIP_TABLE_CREATION: [ RefractorTrack, RefractorCardState, - RefractorTierBoost, - RefractorCosmetic, RefractorBoostAudit, ], safe=True, diff --git a/migrations/2026-04-05_drop_unused_refractor_tables.sql b/migrations/2026-04-05_drop_unused_refractor_tables.sql new file mode 100644 index 0000000..4f91046 --- /dev/null +++ b/migrations/2026-04-05_drop_unused_refractor_tables.sql @@ -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; diff --git a/tests/conftest.py b/tests/conftest.py index d3ec000..2e11617 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -48,8 +48,6 @@ from app.db_engine import ( PitchingCard, RefractorTrack, RefractorCardState, - RefractorTierBoost, - RefractorCosmetic, RefractorBoostAudit, ScoutOpportunity, ScoutClaim, @@ -81,8 +79,6 @@ _TEST_MODELS = [ ScoutClaim, RefractorTrack, RefractorCardState, - RefractorTierBoost, - RefractorCosmetic, BattingCard, PitchingCard, RefractorBoostAudit, diff --git a/tests/test_postgame_refractor.py b/tests/test_postgame_refractor.py index 774e9de..d3569c4 100644 --- a/tests/test_postgame_refractor.py +++ b/tests/test_postgame_refractor.py @@ -72,8 +72,6 @@ from app.db_engine import ( Rarity, RefractorBoostAudit, RefractorCardState, - RefractorCosmetic, - RefractorTierBoost, RefractorTrack, Roster, RosterSlot, @@ -123,8 +121,6 @@ _WP13_MODELS = [ PitchingCardRatings, RefractorTrack, RefractorCardState, - RefractorTierBoost, - RefractorCosmetic, RefractorBoostAudit, ] diff --git a/tests/test_refractor_boost_integration.py b/tests/test_refractor_boost_integration.py index 7635245..621dfae 100644 --- a/tests/test_refractor_boost_integration.py +++ b/tests/test_refractor_boost_integration.py @@ -52,8 +52,6 @@ from app.db_engine import ( Rarity, RefractorBoostAudit, RefractorCardState, - RefractorCosmetic, - RefractorTierBoost, RefractorTrack, Roster, RosterSlot, @@ -111,8 +109,6 @@ _BOOST_INT_MODELS = [ PitchingCardRatings, RefractorTrack, RefractorCardState, - RefractorTierBoost, - RefractorCosmetic, RefractorBoostAudit, ] diff --git a/tests/test_refractor_models.py b/tests/test_refractor_models.py index 782967c..cfce55e 100644 --- a/tests/test_refractor_models.py +++ b/tests/test_refractor_models.py @@ -5,8 +5,6 @@ Covers WP-01 acceptance criteria: - RefractorTrack: CRUD and unique-name constraint - RefractorCardState: CRUD, defaults, unique-(player,team) constraint, 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), and in-place stat accumulation @@ -22,8 +20,6 @@ from playhouse.shortcuts import model_to_dict from app.db_engine import ( BattingSeasonStats, RefractorCardState, - RefractorCosmetic, - RefractorTierBoost, RefractorTrack, ) @@ -134,115 +130,6 @@ class TestRefractorCardState: 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 # --------------------------------------------------------------------------- diff --git a/tests/test_refractor_state_api.py b/tests/test_refractor_state_api.py index 9c81154..e052e3d 100644 --- a/tests/test_refractor_state_api.py +++ b/tests/test_refractor_state_api.py @@ -64,8 +64,6 @@ from app.db_engine import ( ProcessedGame, Rarity, RefractorCardState, - RefractorCosmetic, - RefractorTierBoost, RefractorTrack, Roster, RosterSlot, @@ -681,8 +679,6 @@ _STATE_API_MODELS = [ ProcessedGame, RefractorTrack, RefractorCardState, - RefractorTierBoost, - RefractorCosmetic, ] diff --git a/tests/test_refractor_track_api.py b/tests/test_refractor_track_api.py index bc0fdff..bca6bfc 100644 --- a/tests/test_refractor_track_api.py +++ b/tests/test_refractor_track_api.py @@ -41,8 +41,6 @@ from app.db_engine import ( # noqa: E402 ProcessedGame, Rarity, RefractorCardState, - RefractorCosmetic, - RefractorTierBoost, RefractorTrack, Roster, RosterSlot, @@ -204,8 +202,6 @@ _TRACK_API_MODELS = [ ProcessedGame, RefractorTrack, RefractorCardState, - RefractorTierBoost, - RefractorCosmetic, ]