- forge_runner._run_process_blocking: parse [DRAW] PARSE-SUMMARY lines
and record life totals per seat into state.draw_game_snapshots
- routes/lab.py get_result(): attach draw_game_snapshots to result JSON
under 'drawGames' so the dashboard and analysis scripts can query them
- New services/draw_game_analyzer.py: report which deck held the highest
average life at timeout; used by /api/lab/result and scripts/
- Also lowers the default batch --clock from 6000 → 3000 per game
(30s per-game wall limit for batch mode, down from 60s).
Interactive games can override via req.clock which defaults stay at 6000.
Closes#176
dict() on a types.SimpleNamespace raises TypeError: object is not iterable.
vars() correctly extracts the __dict__ attribute map. Fixes
TestBuildCollectionFailureTracking::test_high_failure_rate_raises_runtime_error.
- _write_build_meta: after writing build_meta.json, also stamps built_at
onto the collection via col.modify() (best-effort, swallows exceptions)
- _collection_is_current: if sidecar built_at is missing/zero, falls back
to collection.metadata["built_at"] so a deleted sidecar doesn't force
an unnecessary full rebuild when the ChromaDB data is still valid
- Add BATCH_RETRY_LIMIT and BATCH_RETRY_DELAY constants after DEFAULT_BATCH_SIZE
- Add _insert_batch_with_retry() helper with exponential backoff
- Replace both bare try/except insert blocks in _build_collection_from_bulk
with calls to the retry helper so transient ChromaDB errors are retried
up to BATCH_RETRY_LIMIT times before a batch is counted as failed
* feat(rag): Phase 2 — ChromaDB vector index over bulk card DB
* feat(rag): add routes/rag.py with build, search, status endpoints
* Update lab_api.pyfeat(rag): register rag_router in lab_api.py, remove old inline rag_status
* Update requirements.txtfeat(rag): add chromadb>=0.5.0 to requirements.txtfeat(rag): add chromadb to requirements.txt
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>
Fixes UnboundLocalError crash when deck_row is None by returning an
empty list instead of falling through to a query with an unbound deck_id.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The guard condition was inverted: `if not deck_row:` entered the query
block when no deck was found, causing `deck_row[0]` to crash with
'NoneType' object is not subscriptable. Flipped to `if deck_row:`
so the query runs only when the deck exists.
- Restored _run_deepseek_batch_thread and _get_deepseek_brain
(accidentally removed in prior commit)
- Changed '-sim' to 'sim' — Forge expects positional mode, not flag
- Add full CSS for precon grid: responsive card grid, hover effects,
install button states, color pips, dark theme styling
- Build commander art_crop image URLs from set_code/number during
precon download (Scryfall direct image redirect, no API call needed)
- Batch-fetch color_identity from Scryfall /cards/collection endpoint
(2 requests for ~145 unique commanders) and store in precon index
- Widen precon modal to 900px for grid layout
- Auto-invalidate precon cache when enrichment fields are missing
- JS renderer now shows commander art crop + card body layout
- Bump cache-busting version to v=4
The PRECON_INDEX list was imported by value in routes/shared.py.
When download_precon_database() or load_precon_index() reassigned it
with 'PRECON_INDEX = ...', the new list object was only visible in
precon_service module scope — shared.PRECON_INDEX (and thus the
/api/lab/precons endpoint) still pointed to the original empty [].
Fix: use PRECON_INDEX.clear() + .extend() instead of reassignment
so all modules sharing the same list reference see the updated data.
- 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
Previously, init_collection_db() opened a separate sqlite3.connect() call,
ran migrations, then closed that connection — entirely outside the
thread-local pool that route handlers use via _get_db_conn().
This meant:
- The migration connection and the request-serving connection were
independent objects with no ordering guarantee under concurrent startup.
- The warm-up connection was thrown away immediately, so every handler
thread still had to open its own connection on first use.
Fix:
- init_collection_db() now calls _get_db_conn() directly, so schema
creation and migrations run on the same thread-local connection that
will serve subsequent requests.
- Added _close_db_conn() helper for explicit teardown on thread exit
(e.g. FastAPI lifespan shutdown or worker thread cleanup).
- Removed the redundant standalone conn.close() that discarded the
init connection without ever making it available to route handlers.
The precon deck list was missing after the monolith refactor because
PRECON_DIR was hardcoded in services/precon_service.py with no CLI arg,
env var, or auto-resolve fallback — unlike forge_jar, forge_dir, etc.
Changes:
- Add precon_dir field to Config class (models/state.py)
- Add --precon-dir CLI arg and PRECON_DIR env var support
- Add _resolve_precon_dir() with project-root, parent-dir, and
forge_dir-relative detection; creates dir as fallback for downloads
- Refactor precon_service.py to use _get_precon_dir() helper that reads
CFG.precon_dir at runtime instead of a hardcoded module-level Path
- Update routes/precon.py to use _get_precon_dir() for install endpoints
- Wire _on_startup to resolve precon_dir for uvicorn-direct launches
- Remove unused PRECON_DIR/PRECON_INDEX imports from routes/lab.py
- Audit confirms all other CFG path fields already have fallbacks
The port of run_batch_subprocess from lab_api_backup.py to
services/forge_runner.py truncated the function signature to 8 params +
**kwargs, but the route handler in routes/lab.py passes 13 positional
arguments, causing TypeError at runtime when POST /api/lab/start fires.
Restore all 13 explicit parameters (policy_style, policy_greedy,
ai_simplified, ai_think_time_ms, max_queue_depth) to match the caller
and pass them as keyword args to build_java_command. Also compute
policy_server from CFG.port and add error handling, matching the
original backup implementation.
Additionally, _run_process_blocking was writing to wrong BatchState
attribute names (state.status, state.games_done, state.games_total,
state.throughput, state.log, state.started_at, state.ended_at) instead
of the correct ones (state.running, state.completed_games,
state.total_games, state.sims_per_sec, state.log_lines, state.elapsed_ms).
This caused the /api/lab/status endpoint to never see progress updates.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>