Commit graph

124 commits

Author SHA1 Message Date
jedarden
39fe612f6a feat(worker): fix rating recovery default sigma value
The rating recovery CLI mode (-mode=recalc-ratings) was using
glicko2Tau (0.5) instead of glicko2DefaultSigma (0.06) for the
default sigma value when resetting ratings. This caused the reset
sigma to be ~8x higher than the schema-defined default.

Added glicko2DefaultSigma constant (0.06) and updated ResetAllRatings
and recalcRatings to use it correctly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 00:49:47 -04:00
jedarden
467b7b67ea feat(worker): add rating recovery CLI mode (-mode=recalc-ratings)
Implements the rating recovery procedure specified in plan §12.3.
Running 'go run ./cmd/acb-worker -mode=recalc-ratings' will:
1. Reset all bot ratings to Glicko-2 defaults (mu=1500, phi=350, sigma=0.06)
2. Fetch all completed matches from the database in chronological order
3. Replay each match to recompute Glicko-2 ratings from scratch
4. Update the bots table with the recalculated ratings

This is needed for disaster recovery when ratings are corrupted or lost.

Database functions added:
- ResetAllRatings: resets all bot ratings to defaults
- GetAllCompletedMatches: fetches completed matches chronologically with participants
- UpdateAllRatings: bulk updates all bot ratings in a single transaction

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 00:41:10 -04:00
jedarden
aeef954590 feat(index-builder): add sitemap.xml generation
Add sitemap.xml generation as a final pass in the index builder. The
sitemap covers all public pages: home, leaderboard, bots list, bot
profiles, matches list, featured replays, seasons, rivalries,
predictions, and docs.

- Add SiteURL config field (ACB_SITE_URL env var, defaults to
  https://aicodebattle.com)
- Add generateSitemap() function with proper XML encoding
- Add SitemapURL and Sitemap types for XML marshaling
- Call generateSitemap() at the end of generateAllIndexes()
- Write sitemap.xml to output directory alongside leaderboard.json

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 00:09:53 -04:00
jedarden
92576dbed4 feat(worker): add map engagement score tracking and verify win_prob in replays
- Add engine.CalculateMapEngagement() to compute map engagement scores from replay data (win_prob_crossings, critical_moments, map_coverage_pct, closeness, turn_pct)
- Add DBClient.UpdateMapEngagement() to update map engagement using rolling average
- Worker now calculates and writes map engagement scores after each match
- Add test to verify win_prob array is non-empty in produced replays

This implements the win probability Monte Carlo array storage in replay JSON
feature. The engine already called ComputeWinProbability() in MatchRunner.Run(),
so this commit adds the missing map engagement tracking.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-03 23:21:57 -04:00
jedarden
42e9561e46 feat(map-evolver): bias energy toward center, carve corridors to force contact
Energy node placement now uses a tiered radius distribution: 30% in the
contested central zone (0.05-0.20 from center), 40% in the mid-zone
(0.20-0.40), and 30% in the home zone (0.40-0.60). Previously nodes were
placed uniformly at 0.20-0.70, letting bots farm their home quadrant
indefinitely without crossing the midline.

After cellular automata wall generation, a 3-wide corridor is carved from
each core straight to the map center, plus a 5x5 open arena at the center
tile. This creates lanes that funnel bots into contact — replicating the key
mechanic that drove frequent fights in the original AI Challenge Ants game,
where symmetric food spawning near the midfield forced both colonies to
expand outward and collide.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 18:56:39 -04:00
jedarden
4937f94afd feat(combat): rank matches by enemy-kill combat turns
Adds combat_turns metric (distinct turns where ≥1 bot died from enemy
focus-fire, excluding self-collisions). Worker computes it after each
match; index builder sorts matches/index.json and the new most-combat
playlist descending by it, and bumps interest score for combat-heavy
matches so they surface in highlights.

Also switches homepage featured replay default view from influence to
standard so the actual bot-on-bot combat is visible.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 18:32:08 -04:00
jedarden
9b16b32aef fix(worker): handle NULL map_json fields with COALESCE
map_json generated by acb-map-evolver lacks a 'spawns' key; scanning
map_json->>'spawns' into a non-nullable string causes "converting NULL
to string is unsupported". Use COALESCE for walls/spawns/cores.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 10:13:30 -04:00
jedarden
181e846d8a feat(map-evolver): bootstrap empty maps table and containerize
- Add seedIfEmpty: idempotent startup seeding (20 maps per player count,
  ON CONFLICT DO NOTHING) using cellular-automata generation + validate()
- Add continuous evolution loop across all player counts (2/3/4/6)
- ACB_MIN_SEED_COUNT and ACB_EVOLUTION_PERIOD configurable via env vars
- Add Dockerfile (lean Alpine build, no language runtimes)
- Add acb-map-evolver to acb-build.yml CI pipeline
- Add staging K8s Deployment manifest

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 08:05:51 -04:00
jedarden
e5dc3bc543 fix: accept base64-encoded AES keys (OpenBao stores keys as base64, not hex)
The encryption key stored in OpenBao/K8s secrets is base64-encoded but
the API and worker crypto functions expected hex. Add parseAESKey() that
accepts both formats (tries hex first, falls back to base64).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 23:04:29 -04:00
jedarden
c726de4d7c fix(test): replace duration-based age check with calendar-relative anchors
TestThreeMonthAgeCheck used 89*24h as "3 months minus 1 day", but
89 calendar days == exactly 3 months on dates like May 1 (Feb+Mar+Apr=
28+31+30=89). The equality case makes the >3-month eligibility check
return true instead of false. Replace with AddDate-relative anchors
so the test stays correct regardless of current date.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 22:14:40 -04:00
jedarden
e64230b122 fix: resolve universal stalemate — signing format and secret decryption
Two root causes prevented bots from making any moves:

1. SignRequest signing string included timestamp ({match_id}.{turn}.{timestamp}.{hash})
   but all bots implement verifySignature without timestamp ({match_id}.{turn}.{hash}).
   Fixed by dropping timestamp from the signing string; X-ACB-Timestamp header is still
   sent for clock-skew checks but not in the HMAC.

2. The API stores bot secrets AES-GCM encrypted (184 hex chars) in the DB. The worker
   was passing the ciphertext directly as the HMAC key, while bots use their plaintext
   k8s secret (64 hex chars). Fixed by decrypting in the worker using ACB_ENCRYPTION_KEY.

Also tightens the home page winner filter to exclude winner_id="0" stalemates.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 21:48:25 -04:00
jedarden
c397e66239 fix(index-builder): run wrangler from /app/web to pick up functions/ bundle
_worker.js static file approach fails — Cloudflare rejects it when uploaded
as a static asset. Instead, copy web/functions/ into the image and set
wrangler CWD to /app/web/ so it discovers functions/ and uploads the Pages
Functions bundle correctly on every deploy cycle.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 12:39:57 -04:00
jedarden
037a04e8b5 fix(index-builder): bundle Pages Functions into _worker.js at build time
Every index-builder deploy was overwriting the production Pages deployment
without functions (wrangler ran from /tmp, no functions/ dir visible).
Compiling functions/ to dist/_worker.js during the Docker web-builder stage
means the worker is always included in every Pages deploy, regardless of CWD.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 12:35:54 -04:00
jedarden
341591a10b fix(worker): disable SDK checksum trailer for R2 uploads
AWS SDK Go v2 s3 v1.100.0 defaults to RequestChecksumCalculationWhenSupported,
which causes PutObject to send STREAMING-UNSIGNED-PAYLOAD-TRAILER — a chunked
transfer mode R2 doesn't support. Setting WhenRequired makes the SDK send a
standard signed payload instead, resolving the 403 SignatureDoesNotMatch.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 10:35:00 -04:00
jedarden
dc0caf0115 feat(worker): upload replays directly to R2 in addition to B2
Adds R2 (Cloudflare) as a direct upload target alongside B2 (cold archive).
When ACB_R2_* credentials are configured, the worker uploads replays and
thumbnails to R2 immediately after each match, bypassing the index-builder's
B2→R2 promotion cycle.

This is necessary because ARMOR's B2 app key is write-only; reads via the
direct S3 path return 403. The Cloudflare CDN read path (armor-hub-b2.ardenone.com)
is dead post-hub-decommission. Direct R2 upload ensures replays are available
without waiting for a working B2 read path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 10:24:47 -04:00
jedarden
d126654dbb fix(worker): use BaseEndpoint instead of EndpointResolverV2 for ARMOR
EndpointResolverV2 with a custom static URI does not honor UsePathStyle —
the resolver provides the final endpoint and the SDK does not re-apply
path-style bucket addressing on top of it. This means the bucket name was
dropped from the request path even with UsePathStyle=true, sending PUTs
to /replays/... instead of /armor-apexalgo/replays/...

BaseEndpoint is the SDK's documented approach for S3-compatible custom
endpoints; it sets the base URL and then correctly applies path-style
addressing to produce /bucket/key URLs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 09:48:53 -04:00
jedarden
55dffb624c fix(worker): UsePathStyle for ARMOR and skip crash_strikes on normal game endings
Two fixes:
1. Add UsePathStyle=true to B2 S3 client. Without it the SDK uses
   virtual-hosted addressing, dropping the bucket name from the request
   path. Uploads hit /replays/... instead of /armor-apexalgo/replays/...
   causing NoSuchBucket errors on every replay/thumbnail PutObject.

2. Don't update crash_strikes for normal game endings (stalemate, turns).
   In snake-style games every bot eventually crashes into a wall/snake —
   that is the expected end condition, not an HTTP error. The old code
   treated all Crashed[] entries from the engine as errors, causing all
   6 bots to accumulate strikes after every single match and hitting the
   30-min cooldown threshold after just 3 matches.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 09:38:09 -04:00
jedarden
626a173e17 test(index-builder): fix buildRivalryNarrative call signature
Added missing aID and bID arguments to match the function
signature in generator.go.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 23:40:57 -04:00
jedarden
b13f98416b fix(api): add missing replay_feedback FK migration
The replay_feedback table was missing its foreign key constraint to
matches(match_id). This happened because CREATE TABLE IF NOT EXISTS
doesn't add FKs to existing tables.

Added an idempotent migration that checks for the constraint's existence
before adding it, ensuring it's safe to run on both fresh installs and
existing databases.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 23:36:02 -04:00
jedarden
ee8c7c37b2 fix(worker): use EndpointResolverV2 for ARMOR B2 uploads
The BaseEndpoint approach with older aws-sdk-go-v2 causes
"Invalid region: region was not a valid DNS name" errors when
uploading to ARMOR's S3-compatible endpoint.

Switching to EndpointResolverV2 bypasses the SDK's endpoint
rule validation entirely, resolving the issue.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 23:33:56 -04:00
jedarden
c9cdafe9ca fix(worker): upgrade aws-sdk-go-v2 to fix B2 upload error
Fixes 'Invalid region: region was not a valid DNS name' error when
uploading replays to B2 via ARMOR proxy.

The error was caused by a known bug in aws-sdk-go-v2 v1.41.4 where
the endpoint resolver would validate the region as a DNS name even
when using a custom BaseEndpoint with UsePathStyle=true.

Upgraded SDK versions:
- github.com/aws/aws-sdk-go-v2 v1.41.4 -> v1.41.6
- github.com/aws/aws-sdk-go-v2/config v1.32.12 -> v1.32.16
- github.com/aws/aws-sdk-go-v2/service/s3 v1.97.2 -> v1.100.0
- github.com/aws/smithy-go v1.24.2 -> v1.25.1

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 22:56:47 -04:00
jedarden
3ae35ea00a test(web): verify match list page renders cards with real matches
- Verified /watch/replays shows real completed matches (not just demo)
- Match cards display bot names, turn count, winner badges, map ID
- 'Watch Replay' links point to real match IDs (m_test_*)
- Curated playlists render with real data (featured, comebacks, upsets, etc.)
- Pagination/infinite scroll works via IntersectionObserver
- Mobile testing on Pixel 6 via ADB: layout responsive, touch targets usable
- Created MATCH_LIST_TEST_RESULTS.md with full verification details
- Thumbnails not implemented (clean UI without broken images due to R2 issues)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 11:42:47 -04:00
jedarden
09fced7dfe fix(worker,index-builder): use us-east-1 region for S3-compatible endpoints
The AWS SDK requires a valid AWS region name even when using custom
S3-compatible endpoints (ARMOR/B2). Using "auto" as the region causes
an error: "Invalid region: region was not a valid DNS name."

This fixes the replay upload pipeline which was failing with the
invalid region error. Replays should now upload successfully to B2
via the ARMOR proxy.

Related to ai-code-battle-o43: Replay viewer verification task.
2026-04-25 11:07:08 -04:00
jedarden
cd30484e8c verify(blog): verify blog page generates and renders AI match commentary posts
Verification results:
1.  /data/blog/index.json exists and has 1 post (meta-week-13-season-1)
2.  Individual post pages load correctly at /blog/{slug}
3.  Blog post JSON structure matches frontend expectations (content_md field)
4.  Tags and filters implemented in UI (All, Meta Reports, Chronicles buttons)
5.  Blog page builds successfully (blog-D4QMd11d.js included in build)

Current state: Blog infrastructure is fully implemented with:
- LLM-powered narrative generation (blog.go, narrative.go)
- Story arc detection (rise, fall, rivalry, upset, evolution milestones)
- Weekly meta report generation with ELO movers, strategy analysis
- Chronicles for story arcs (rivalry, upset, rise/fall, evolution)
- Tag-based filtering and search

Note: Current blog content is placeholder/template-based. Meaningful
match commentary will be generated when:
- ACB_LLM_BASE_URL and ACB_LLM_API_KEY are configured in index-builder
- Real match data exists in PostgreSQL database
- Story arcs are detected from rating history and match results
2026-04-25 10:40:36 -04:00
jedarden
e601fecc04 fix(worker): update B2 client for S3-compatible API (ARMOR/B2)
Remove custom endpoint resolver and use AWS SDK's standard approach
for S3-compatible endpoints:
- Use config.WithRegion("auto") for custom endpoints
- Set BaseEndpoint directly via s3.NewFromConfig options
- Add UsePathStyle for B2 compatibility

This fixes the 'Invalid region: region was not a valid DNS name' error
that was preventing replay uploads. The deployment manifest already
sets ACB_B2_REGION to empty string to avoid conflicts.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 10:38:41 -04:00
jedarden
41c7223f8a refactor(web): promote app.html to index.html as homepage
The main leaderboard SPA is now served at / (index.html) and the
standalone replay viewer lives at /replay.html. This removes the
_redirects workaround in index-builder that patched over the inverted
entry points.

- Rename web/app.html → web/index.html (main SPA)
- Rename web/index.html → web/replay.html (standalone viewer)
- Update vite.config.ts: main→index.html, replay→replay.html
- Remove _redirects injection from deploy.go verifyMergedOutput
- Update pages.json routes and README dev URL

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 08:51:52 -04:00
jedarden
5d89fbd885 fix(index-builder): add _redirects to route / → app.html
The replay viewer was baked into index.html (served at /) while the
leaderboard app was at /app.html. Add a _redirects file so visitors
landing on / get redirected to the main leaderboard app.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 07:22:06 -04:00
jedarden
992fa1d573 fix(worker): crash cooldown passes time.Time not time.Duration to pq
Passing time.Duration (int64 nanoseconds) as $2 in NOW() + $2 caused
PostgreSQL to interpret the nanosecond value as seconds, setting
cooldown_until to year ~59066 instead of +30 minutes.

Fix: pre-compute time.Now().Add(CrashCooldownDuration) and pass the
resulting time.Time — pq encodes it as a proper timestamptz.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 18:07:29 -04:00
jedarden
4f45670066 fix(worker): use seasons.id instead of seasons.season_id in ClaimJob
The seasons table was recreated with id BIGSERIAL (not season_id VARCHAR).
The ClaimJob query was still referencing s.season_id (stale column name).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 17:39:24 -04:00
jedarden
38ae4c6303 fix(worker): use winner identity for Glicko-2 pairwise scoring
Raw game scores (capture points) are tied in most matches since the
winner is determined by an energy/bots-alive tiebreaker. This caused
Glicko-2 delta=0, leaving rating_mu frozen at 1500 for all bots.

Now winner gets 1.0, non-winners 0.0, draws 0.5 — correct pairwise
win/loss signal for Glicko-2 convergence.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 17:29:36 -04:00
jedarden
ea2f1b50b7 fix(matchmaker): stale-reaper queries claimed not running status
Jobs remain in 'claimed' status until completed — the reaper was
querying 'running' (which is the match status, not job status) so
stale claimed jobs were never recycled.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 17:24:57 -04:00
jedarden
9c5eb57fdd fix(evolver): correct GROUP BY in island stats query
b.bot_id was selected without being in the GROUP BY clause or wrapped
in an aggregate, causing a Postgres error on live export. Replaced with
a correlated subquery that finds the highest-rated bot per island.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 07:04:37 -04:00
jedarden
b7990ad475 fix(docker): add COPY ratelimit/ to acb-api Dockerfile
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 18:09:53 -04:00
jedarden
d0087a3241 fix(docker): add COPY metrics/ to all service Dockerfiles
The metrics package is a local module dependency imported by all services
but was missing from every Dockerfile's build context.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 18:00:16 -04:00
jedarden
002f945298 fix(matchmaker): use inclusive boundary for classic promotion age check
90 days == 3 calendar months exactly in March/April, causing
TestThreeMonthAgeCheck to fail. The intent is >= 3 months old.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 17:45:36 -04:00
jedarden
0813e36297 fix(evolver): wire Nash mixture and meta weaknesses into LLM prompts, fix 4-D diversity
- Add NashMixture and MetaWeaknesses fields to meta.Description and
  compute them from island population proportions (§10.2 PSRO)
- Update behaviorDistance to support N-D vectors for 4-D MAP-Elites
  grid (aggression, economy, exploration, formation)
- Wire NashMixture/MetaWeaknesses through FromMetaDescription converter
  so they actually reach the LLM prompt (was dead code before)
- Align LLM prompt with plan §15.1/§15.5: correct combat rules
  (focus-fire), fog of war, HTTP protocol section, Nash mixture target
- Fix diversity normalization from sqrt(2) (2-D) to 2.0 (4-D max)
- Rename handleUIFeedback to handleCreateFeedback (§13.6 naming)
- Update tests for new fields and corrected prompt text

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-23 01:22:19 -04:00
jedarden
be66af1ad5 fix(narrative): add season championship context and §13.2 critical moments to spotlight prompt
Add formatSeasonChampionshipContext helper to inject season progress,
championship bracket positioning, and seed lines into LLM prompts.
Add §13.2 critical moment / turning point summary to the match-of-the-week
section of buildSpotlightPrompt. These complete the §15.1/§15.5 alignment
for structured contextual match data in sports-journalism-style prompts.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 20:08:49 -04:00
jedarden
922ef5cd5a fix(narrative): align LLM prompts with plan §15.1/§15.5 sports-journalism spec
Restore detailed system prompt constant framing the LLM as a sports
journalist covering an emergent bot league, with specific guidance on
ELO deltas, rivalry context, head-to-head records, and scouting-style
lineage framing. Enrich per-arc prompts with critical moment summaries
(§13.2), community tactical hints, ELO before/after deltas, and
head-to-head records. Fix rivalry arc to include ELO context for both
bots. Ensure fall arc shows both wins and losses in key match listings.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 19:40:49 -04:00
jedarden
ae8eb0465e feat(narrative): add critical moment summaries to key match extraction
Generates contextual turning-point descriptions for matches used in blog
narratives and rivalry chronicles (§13.2). Summarizes close scores, ELO
upsets, non-standard end conditions, and marathon matches.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 19:08:50 -04:00
jedarden
fddcc0ba34 fix(index-builder): add missing getCurrentSeasonTheme and buildHeadToHeadFromArc
Two functions referenced in generateLLMChronicle were undefined:
- getCurrentSeasonTheme: returns the active season's theme string
- buildHeadToHeadFromArc: computes W/L head-to-head records for a bot
  against all opponents from match data, enriching LLM narrative prompts

Also improves the sports journalist system prompt with more detailed
coverage style guidance for better narrative quality.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 19:01:03 -04:00
jedarden
7978ebbab3 feat(§15.2): generate and stream static meta JSON files to R2
- Add data/meta/rivalries.json to R2 upload list in uploadMetaJSONToR2
- Add attachCommunityHints() to narrative.go to enrich story arcs with
  highest-upvote community tactical hints (upvotes >= 3, idea/mistake types)
- Fix detectRivalryArcs() key separator from "-" to "|" to avoid UUID
  hyphen collisions when parsing bot ID pairs
- Fix partitionBots() call sites in bot_strategies_phase13.go to use
  struct field access (.friendly, .enemy) matching updated return type

generator.go already contains generateArchetypes, generateCommunityHints,
and generateMatchFeedback (all called from generateAllIndexes). main.go
uploads all four outputs to R2 on every build cycle.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 18:46:27 -04:00
jedarden
60b83a02d9 feat(§15.3): implement screen reader transcript for replay viewer
- Add transcript panel with turn-by-turn summaries generated from replay events
- Each turn shows: player moves, combat, deaths, captures, energy collection, spawns, win probability
- Add 'T' key shortcut to toggle transcript panel
- Panel supports three view modes: All Turns, ±10 Turns from Current, Recent 20 Turns
- Click on transcript entry to jump to that turn
- Current turn is highlighted in transcript with smooth scroll
- Panel content is selectable/copyable for screen reader users
- Transcript generation logic already existed in replay-viewer.ts; this adds the UI
- Transcript button slides in from right side of screen

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 18:42:49 -04:00
jedarden
38f14e1997 fix: remove unused imports in evolver, misc pre-dispatch changes
Remove unused encoding/json and net/http imports from cmd/acb-evolver/run.go
that caused build failure. Include other pre-dispatch changes from prior work.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 18:32:46 -04:00
jedarden
a509b70800 feat(§15.3): implement screen reader transcript for replay viewer
- Add ARIA live region announcement during auto-playback using detailed transcript text
- Transcript panel shows turn-by-turn summaries with current turn highlighting
- T key toggles transcript panel (collapsible UI)
- Panel content is selectable/copyable text for screen reader users
- Fix build errors in clip-maker.ts (remove unused lastExportBlob references)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 18:30:18 -04:00
jedarden
98a9f645c4 feat(evolver): update retirement ticker interval to daily (§10.8)
Changed RetirementCheckInterval from 1 hour to 24 hours to align
with the 7-day low-rating rule specified in §10.8. The retirement
automation is already fully implemented:

- startRetirementTicker: runs periodic checks (now daily)
- EnforcePolicy: retires bots below rating threshold (800) for 7
  consecutive days, enforces 50-bot population cap
- queryConsecutiveLowRating: uses rating_history table to track
  consecutive days below threshold
- RetireBot: handles K8s manifest deletion via declarative-config
- TestEnforcePolicy_CapEnforcement: integration test for cap enforcement

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 18:23:03 -04:00
jedarden
88bd70640a fix(types): add missing ReplayPlayer import and type annotation for transcript feature
- Add ReplayPlayer to type imports in replay-viewer.ts
- Add explicit type annotation for entry parameter in replay.ts transcript map
- Fixes TypeScript compilation errors for §15.3 screen reader transcript feature
2026-04-22 18:20:56 -04:00
jedarden
3b94b7eccb feat(matchmaker): add map fairness monitoring and auto-retirement (§14.6)
Implements the full map lifecycle audit as a hourly ticker in the
matchmaker:

1. updateMapFairnessStats: recompute per-slot win counts from completed
   matches into the map_fairness table
2. flagUnfairMaps: flag maps where any slot deviates >10pp from expected
   (1/N) across 80+ matches → status='probation'
3. retireDislikedMaps: force-retire maps with >20 net negative votes
4. pruneLowEngagementMaps: monthly bottom-10% engagement prune per tier
5. promoteClassicMaps: top-5 all-time engagement, 3+ months → 'classic'

Matchmaker already filters retired maps and gives probation maps 50%
reduced selection probability in selectMapLRU.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 18:19:24 -04:00
jedarden
6c1f031071 feat(config): add season_id + rules_version to Config per §4.2
- SeasonID and RulesVersion already present in engine/types.go Config struct
- Worker already populates from active season row via DB join
- Config embedded in VisibleState sent to bots each turn (including turn 0)
- All starter kits (go, python, rust, java, csharp) already expose and log fields
- Add season_id/rules_version logging to JavaScript starter on turn 0
- TypeScript Config interface already includes season_id and rules_version

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 18:09:26 -04:00
jedarden
4d1f5f976d feat(index): implement §14.5 predictions/open.json static file generation
Adds generatePredictionsOpen to acb-index-builder, writing:
- data/predictions/open.json — upcoming predictable matches

Queries pending matches that are "predictable":
- Both bots in top 20
- Rivalry matches (3+ previous h2h matches)
- Series matches
- Evolved bot vs top-10 human-written bot

Output schema:
- updated_at — timestamp of generation
- matches — array of {match_id, bot_a, bot_b, a_rating, b_rating, open_until, head_to_head_record}

Limits to next 10 matches to keep file small. Refreshes every ~15 min by the index builder.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 17:53:48 -04:00
jedarden
32d7dd07e7 feat(index-builder): merge latest site build artifact before Pages deploy (§8.4, §11.3)
Add crane CLI to the runtime Dockerfile so the index builder can pull the
latest SPA shell from the Forgejo container registry on each cycle. The
existing syncSiteBuild logic checks for a newer image digest, extracts
the dist/ assets via crane export, and overlays generated JSON data files
on top before deploying to Cloudflare Pages.

- Dockerfile: install go-containerregistry crane binary (v0.20.2)
- sitebuild.go: new file with syncSiteBuild, craneDigest, craneExport,
  digest caching, fallback to baked-in /app/web/dist
- main.go: wire initCraneAuth at startup, replace hardcoded webDistDir
  with syncSiteBuild call in runBuildCycle
- sitebuild_test.go: 18 tests for extractRegistry, digest caching,
  fallback logic, crane auth config, and copyWebAssets overlay behavior

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 17:52:03 -04:00