* feat(83): add PolicyClient for live Forge-to-policy-server IPC (#83)
Phase 2.1: Bridge between Forge decision points and Python policy
server for online training. Calls /api/policy/decide endpoint,
collects (state, action, reward) tuples in real-time, and flushes
them to the server for online PPO updates.
Falls back to Forge built-in AI if server is unavailable.
* feat(83): add /api/policy/* routes for live Forge IPC (#83)
Phase 2.2: Policy decision routes for real-time Forge IPC.
- POST /api/policy/decide — full game state inference with log_prob/value
- POST /api/policy/collect — online learning tuple batch collection
- POST /api/policy/reward — end-of-game reward propagation
- GET /api/policy/health — readiness check for Java PolicyClient
- GET /api/policy/stats — session statistics
Includes Forge phase mapping and PlayerZoneState with full zone contents.
* feat(83): integrate PolicyClient into GameSession for bidirectional sync (#83)
Create services/logging.py as the canonical home for setup_logging(),
all log format constants, named loggers, and _ml_logging_enabled.
Update all route modules and lab_api.py to import logging symbols from
services.logging and all other symbols from their canonical sources
(models.state, models.requests, models.responses, services.database,
services.deck_service, services.scryfall, services.import_helpers,
services.import_service, services.precon_service, services.forge_runner).
Delete routes/shared.py — the re-export hub is no longer needed.
Closes#61
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The hardcoded 192.168.0.240 IP was a developer-specific LAN address
that breaks out-of-the-box for anyone running Ollama locally or via
WSL2 on Windows. Switch every default and docstring reference to
localhost so the project works without manual configuration.
Files changed:
- coach/config.py: LLM_URL and DECK_GEN_BASE_URL defaults
- ui/simulator.html: API endpoint input value and placeholder
- lab_api.py: remove LAN IP from default CORS origins
- routes/coach.py: warning message text
- src/commander_ai_lab/sim/deepseek_brain.py: docstrings
- src/commander_ai_lab/sim/deepseek_engine.py: docstring example
- src/commander_ai_lab/sim/validator_brain.py: docstring example
Co-authored-by: Claude Code <noreply@anthropic.com>
_get_validator_brain() defaulted VALIDATOR_MODEL to deepseek-r1:14b
and VALIDATOR_TIMEOUT to 120.0, conflicting with the brain config.
Changed to deepseek-r1:8b and 300.0 to match. Fixes issue #95 item 4.
Implements the three approved Phase 5 architecture decisions:
1. Zone embedding boundary: add explicit documentation in StateDimensions
and StateEncoder._encode_zones making the 6144-dim zero-padded zone
contract visible to future maintainers.
2. SQLite WAL online learning store: new OnlineLearningStore class backed
by WAL-mode SQLite for concurrent read/write on single-machine baseline.
New /api/policy/decide endpoint collects decisions during inference.
Stats endpoint at /api/policy/decide/stats.
3. Forge-only default weights: change ppo_weight default from 0.5 to 0.0
across scope.py, DistillationConfig, DatasetConfig, and CLI defaults.
Add MIXED_MODE_PRESETS dict with forge_only, forge_90_10, forge_80_20
presets. Distill/start endpoint now accepts a "preset" parameter.
27 new tests covering all three decisions (all passing).
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The distillation pipeline was looking for JSONL decision files in
data/results/ but they are stored in results/ (matching CFG.results_dir).
This caused all generations to fail at Step 1 with:
'No ml-decisions-*.jsonl files found in data/results'
Fixed:
- routes/ml.py: results_dir and history_dir now use 'results/' not 'data/results/'
- ml/training/distillation_loop.py: updated DistillationConfig defaults to match
- History endpoint paths updated to match
- Create ml/eval/elo_tracker.py:
- EloTracker class with standard ELO formula (K=32, start=1200)
- EloResult dataclass for tournament output
- EloHistory class for JSON persistence (data/elo_history.json)
- run_elo_tournament(): round-robin ELO-rated matches using
existing run_match() from tournament.py
- run_generation_tournament(): discovers gen-*/best_policy.pt
checkpoints and runs cross-generation ELO tournament
- CLI entry point: python -m ml.eval.elo_tracker
- Add ELO API endpoints to routes/ml.py:
- POST /api/ml/elo/tournament — start background ELO tournament
- GET /api/ml/elo/tournament/status — poll progress
- GET /api/ml/elo/history — load ELO history from disk
- Integrate ELO convergence in distillation_loop.py:
- New config fields: elo_convergence_enabled (default False),
elo_convergence_threshold (20.0), elo_tournament_episodes (30)
- New field: GenerationRecord.elo_rating
- _run_elo_mini_tournament() runs after quality gate pass
- _check_convergence() checks ELO delta when enabled
- Add ELO UI to distillation page:
- ELO Ratings panel with canvas multi-line chart showing per-model
ELO progression across generations
- Run Tournament button with progress indicator
- ELO leaderboard table with rank, rating, and visual bars
- Responsive chart resize handling
Closes#70
- Add distillation API endpoints to routes/ml.py:
- POST /api/ml/distill/start — start closed-loop distillation pipeline
- GET /api/ml/distill/status — poll loop status and generation data
- POST /api/ml/distill/stop — graceful stop after current generation
- GET /api/ml/distill/history — load persisted generation history
- Add 'Distill' nav entry in ui/nav.js with loop icon
- Create ui/distillation.html:
- Status overview cards (status, generation, best win rate, gate passes)
- Control panel with hyperparameter inputs for supervised, PPO, and
quality gate settings
- Start/Stop buttons with progress bar and phase badge
- Win rate trend chart (canvas-based, no dependencies)
- Data composition stacked bar chart (Forge vs PPO per generation)
- Generation history table with status, metrics, and gate results
- Create ui/distillation.js:
- Polling loop for real-time status updates during distillation
- Canvas chart rendering for win rate trends and data composition
- History table rendering with color-coded status tags
- Responsive resize handling for charts
- Create ui/distillation.css:
- Page-specific styles following training.css patterns
- Chart container, history table, and responsive grid styles
Closes#69
Extract player_explanation from validation dict in each turn log
entry within _run_game_loop(). The React frontend can now render
validator explanations when player_explanation is non-null.
- Sonar does not support response_format type json_schema
- Switch to {"type": "json_object"} which Sonar supports
- Keep json_schema/schema_name params for call-site compatibility (ignored)
- Add exc_info=True to V3 generate error logging for full tracebacks
collection.py re-defined _ScryfallCache, _scryfall_cache,
_scryfall_rate_limit, _fetch_scryfall_api, _enrich_from_scryfall,
_parse_finish, _parse_text_line, _auto_infer_mapping, _parse_csv_content,
_TYPE_PRIORITY, and _TYPE_TARGETS locally (lines 333-509).
These local definitions shadowed the correct imports from
routes/shared.py -> services/scryfall.py. Critically, the local
_fetch_scryfall_api used Request and urlopen without importing them,
causing a NameError at runtime that was silently swallowed by the
broad except clause — making every card fail Scryfall enrichment
during collection import.
Removed all ~470 lines of duplicate code. The import endpoint now
correctly uses the functions imported from shared.py, which have
proper urllib imports and a working Scryfall cache.
- Replace blocking urlopen with async httpx.AsyncClient
- Add await at both call sites (deck_research, deck_generate_ai)
- Uvicorn event loop no longer stalled during Perplexity API calls
- Replace SELECT * + Python-side color loop in _get_collection_for_colors()
with projected SELECT + NOT EXISTS json_each() sub-query
- Apply same pattern to _build_collection_summary(), removing card_fits_identity()
- Add partial index idx_ce_quantity on collection_entries(quantity) WHERE qty > 0
- Handle NULL and empty-string color_identity via CASE guard
- Fix colorless commander bug: old code skipped color check entirely for []
- Add scripts/bench_deckgen.py benchmark (3-5x speedup on filtered queries)
- Add tests/test_deckgen_color_filter.py with 7 regression tests
- Add @dataclass classes _TrainingState, _PPOState, _TournamentState with snapshot() method
- Wrap all state mutations in threading.Lock() for thread safety
- Replace dict bracket access with attribute access
- Remove unnecessary global declarations from pipeline functions