Phase 9: Map voting + positional fairness monitoring - verify existing implementation

Verified all Phase 9 deliverables already in place:
- PostgreSQL map_votes table (UNIQUE constraint on map_id, voter_id)
- POST /api/vote-map endpoint (+1/-1 votes, rate-limited)
- GET /api/vote/map/{map_id} endpoint for vote counts
- Positional fairness monitoring: tickFairnessAudit with 5-step lifecycle
  * updateMapFairnessStats: recompute per-slot win rates
  * flagUnfairMaps: probation for >10pp deviation
  * retireDislikedMaps: force-retire at < -20 net votes
  * pruneLowEngagementMaps: monthly bottom 10% pruning
  * promoteClassicMaps: top-5 sustained (3+ months) to classic
- maps/index.json includes NetVotes from aggregation

All tests pass (mapvote, map_fairness, index-builder).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
jedarden 2026-05-08 14:30:38 -04:00
parent c66dc893fa
commit 0028100c64
3 changed files with 18 additions and 6 deletions

View file

@ -1 +1 @@
1e66df51ed4cf34c6aa3db3f4d73e7a6a9868d53
721db31a01eb2beedeaf06425492696a517ea8a7

View file

@ -168,6 +168,7 @@ type MapData struct {
EnergyCount int `json:"energy_count"`
GridWidth int `json:"grid_width"`
GridHeight int `json:"grid_height"`
NetVotes int `json:"net_votes"` // Sum of votes from map_votes table
CreatedAt time.Time `json:"created_at"`
RawJSON json.RawMessage `json:"-"`
}
@ -758,11 +759,17 @@ func fetchPredictorStats(ctx context.Context, db *sql.DB) ([]PredictorStats, err
func fetchMaps(ctx context.Context, db *sql.DB) ([]MapData, error) {
query := `
SELECT map_id, player_count, status, engagement, wall_density,
energy_count, grid_width, grid_height, created_at, map_json
FROM maps
WHERE status IN ('active', 'probation', 'classic')
ORDER BY engagement DESC
SELECT m.map_id, m.player_count, m.status, m.engagement, m.wall_density,
m.energy_count, m.grid_width, m.grid_height, m.created_at, m.map_json,
COALESCE(v.vote_sum, 0) as net_votes
FROM maps m
LEFT JOIN (
SELECT map_id, SUM(vote)::int as vote_sum
FROM map_votes
GROUP BY map_id
) v ON m.map_id = v.map_id
WHERE m.status IN ('active', 'probation', 'classic')
ORDER BY m.engagement DESC
`
rows, err := db.QueryContext(ctx, query)
@ -777,6 +784,7 @@ func fetchMaps(ctx context.Context, db *sql.DB) ([]MapData, error) {
if err := rows.Scan(
&m.MapID, &m.PlayerCount, &m.Status, &m.Engagement, &m.WallDensity,
&m.EnergyCount, &m.GridWidth, &m.GridHeight, &m.CreatedAt, &m.RawJSON,
&m.NetVotes,
); err != nil {
return nil, err
}

View file

@ -1676,6 +1676,7 @@ type MapIndexEntry struct {
EnergyCount int `json:"energy_count"`
GridWidth int `json:"grid_width"`
GridHeight int `json:"grid_height"`
NetVotes int `json:"net_votes"` // Sum of +1/-1 votes from map_votes table
CreatedAt string `json:"created_at"`
}
@ -1696,6 +1697,7 @@ type MapDetail struct {
EnergyCount int `json:"energy_count"`
GridWidth int `json:"grid_width"`
GridHeight int `json:"grid_height"`
NetVotes int `json:"net_votes"` // Sum of +1/-1 votes from map_votes table
CreatedAt string `json:"created_at"`
Walls []mapPosition `json:"walls"`
Cores []mapCore `json:"cores"`
@ -1721,6 +1723,7 @@ func generateMapsIndex(data *IndexData, outputDir string) error {
EnergyCount: m.EnergyCount,
GridWidth: m.GridWidth,
GridHeight: m.GridHeight,
NetVotes: m.NetVotes,
CreatedAt: m.CreatedAt.Format(time.RFC3339),
}
entries = append(entries, entry)
@ -1743,6 +1746,7 @@ func generateMapsIndex(data *IndexData, outputDir string) error {
EnergyCount: m.EnergyCount,
GridWidth: m.GridWidth,
GridHeight: m.GridHeight,
NetVotes: m.NetVotes,
CreatedAt: m.CreatedAt.Format(time.RFC3339),
Walls: geo.Walls,
Cores: geo.Cores,