From 84749da214d9b5f771a850bf96cce1ede0f52ba3 Mon Sep 17 00:00:00 2001 From: matt Date: Thu, 2 Oct 2025 16:28:19 -0700 Subject: [PATCH] docs: refresh docker and readme guides --- DOCKER.md | 424 ++++++------- README.md | Bin 123260 -> 29874 bytes docs/archive/DOCKER_2025-10-02.md | 228 +++++++ docs/archive/README_2025-10-02.md | 951 ++++++++++++++++++++++++++++++ docs/headless_cli_guide.md | 91 +++ docs/theme_catalog_advanced.md | 148 +++++ docs/web_ui_deep_dive.md | 103 ++++ 7 files changed, 1737 insertions(+), 208 deletions(-) create mode 100644 docs/archive/DOCKER_2025-10-02.md create mode 100644 docs/archive/README_2025-10-02.md create mode 100644 docs/headless_cli_guide.md create mode 100644 docs/theme_catalog_advanced.md create mode 100644 docs/web_ui_deep_dive.md diff --git a/DOCKER.md b/DOCKER.md index 98ea6cc..c11e54a 100644 --- a/DOCKER.md +++ b/DOCKER.md @@ -1,228 +1,236 @@ # Docker Guide -Run the MTG Deckbuilder (CLI and Web UI) in Docker with persistent volumes and optional headless mode. +Spin up the MTG Python Deckbuilder inside containers. The image defaults to the Web UI; switch to the CLI/headless runner by flipping environment variables. All commands assume Windows PowerShell. -## Quick start +## Build a Deck (Web UI) -### PowerShell (recommended) -```powershell -docker compose build -docker compose run --rm mtg-deckbuilder -``` +- Build the image (first run only) and start the `web` service in detached mode: -### From Docker Hub (PowerShell) -```powershell -docker run -it --rm ` - -v "${PWD}/deck_files:/app/deck_files" ` - -v "${PWD}/logs:/app/logs" ` - -v "${PWD}/csv_files:/app/csv_files" ` - -v "${PWD}/owned_cards:/app/owned_cards" ` - -v "${PWD}/config:/app/config" ` - mwisnowski/mtg-python-deckbuilder:latest -``` - -## Web UI (new) - -The web UI runs the same deckbuilding logic behind a browser-based interface. - -### PowerShell (recommended) ```powershell docker compose up --build --no-deps -d web ``` -Then open http://localhost:8080 +- Open http://localhost:8080 to use the browser experience. First launch seeds data, downloads the latest card catalog, and tags cards automatically (`WEB_AUTO_SETUP=1`, `WEB_TAG_PARALLEL=1`, `WEB_TAG_WORKERS=4` in `docker-compose.yml`). -Volumes are the same as the CLI service, so deck exports/logs/configs persist in your working folder. -The app serves a favicon at `/favicon.ico` and exposes a health endpoint at `/healthz`. -Compare view offers a Copy summary button to copy a plain-text diff of two runs. The sidebar has a subtle depth shadow for clearer separation. - -Web UI feature highlights: -- Locks: Click a card or the lock control in Step 5; locks persist across reruns. -- Replace: Enable Replace in Step 5, click a card to open Alternatives (filters include Owned-only), then choose a swap. -- Permalinks: Copy a permalink from Step 5 or a Finished deck; paste via “Open Permalink…” to restore. -- Compare: Use the Compare page from Finished Decks; quick actions include Latest two and Swap A/B. - -Virtualized lists and lazy images (opt‑in) -- Set `WEB_VIRTUALIZE=1` to enable virtualization in Step 5 grids/lists and the Owned library for smoother scrolling on large sets. -- Example (Compose): - ```yaml - services: - web: - environment: - - WEB_VIRTUALIZE=1 - ``` -- Example (Docker Hub): - ```powershell - docker run --rm -p 8080:8080 ` - -e WEB_VIRTUALIZE=1 ` - -v "${PWD}/deck_files:/app/deck_files" ` - -v "${PWD}/logs:/app/logs" ` - -v "${PWD}/csv_files:/app/csv_files" ` - -v "${PWD}/owned_cards:/app/owned_cards" ` - -v "${PWD}/config:/app/config" ` - -e SHOW_DIAGNOSTICS=1 ` # optional: enables diagnostics tools and overlay - mwisnowski/mtg-python-deckbuilder:latest ` - bash -lc "cd /app && uvicorn code.web.app:app --host 0.0.0.0 --port 8080" - ``` - -### Diagnostics and logs (optional) -Enable internal diagnostics and a read-only logs viewer with environment flags. - -- `SHOW_DIAGNOSTICS=1` — adds a Diagnostics nav link and `/diagnostics` tools -- `SHOW_LOGS=1` — enables `/logs` and `/status/logs?tail=200` - -When enabled: -- `/logs` supports an auto-refresh toggle with interval, a level filter (All/Error/Warning/Info/Debug), and a Copy button to copy the visible tail. -- `/status/sys` returns a simple system summary (version, uptime, UTC server time, and feature flags) and is shown on the Diagnostics page when `SHOW_DIAGNOSTICS=1`. - - Virtualization overlay: press `v` on pages with virtualized grids to toggle per-grid overlays and a global summary bubble. - -Compose example (web service): -```yaml -environment: - - SHOW_LOGS=1 - - SHOW_DIAGNOSTICS=1 -``` - -Docker Hub (PowerShell) example: -```powershell -docker run --rm ` - -p 8080:8080 ` - -e SHOW_LOGS=1 -e SHOW_DIAGNOSTICS=1 -e ENABLE_THEMES=1 -e THEME=system ` - -e SPLASH_ADAPTIVE=1 -e SPLASH_ADAPTIVE_SCALE="1:1.0,2:1.0,3:1.0,4:0.6,5:0.35" ` # optional experiment - -e RANDOM_MODES=1 -e RANDOM_UI=1 -e RANDOM_MAX_ATTEMPTS=5 -e RANDOM_TIMEOUT_MS=5000 ` - -v "${PWD}/deck_files:/app/deck_files" ` - -v "${PWD}/logs:/app/logs" ` - -v "${PWD}/csv_files:/app/csv_files" ` - -v "${PWD}/owned_cards:/app/owned_cards" ` - -v "${PWD}/config:/app/config" ` - mwisnowski/mtg-python-deckbuilder:latest ` - bash -lc "cd /app && uvicorn code.web.app:app --host 0.0.0.0 --port 8080" -``` - -### Setup speed: parallel tagging (Web) -First-time setup or stale data triggers card tagging. The web service uses parallel workers by default. - -Configure via environment variables on the `web` service: -- `WEB_TAG_PARALLEL=1|0` — enable/disable parallel tagging (default: 1) -- `WEB_TAG_WORKERS=` — number of worker processes (default: 4 in compose) - -If parallel initialization fails, the service falls back to sequential tagging and continues. - -### From Docker Hub (PowerShell) -If you prefer not to build locally, pull `mwisnowski/mtg-python-deckbuilder:latest` and run uvicorn: -```powershell -docker run --rm ` - -p 8080:8080 ` - -v "${PWD}/deck_files:/app/deck_files" ` - -v "${PWD}/logs:/app/logs" ` - -v "${PWD}/csv_files:/app/csv_files" ` - -v "${PWD}/owned_cards:/app/owned_cards" ` - -v "${PWD}/config:/app/config" ` - mwisnowski/mtg-python-deckbuilder:latest ` - bash -lc "cd /app && uvicorn code.web.app:app --host 0.0.0.0 --port 8080" -``` - -Health check: -```text -GET http://localhost:8080/healthz -> { "status": "ok", "version": "dev", "uptime_seconds": 123 } -``` - -Theme preference reset (client-side): use the header’s Reset Theme control to clear the saved browser preference; the server default (THEME) applies on next paint. - -### Random Modes (alpha) and test dataset override - -Enable experimental Random Modes and UI controls in Web runs by setting: - -```yaml -services: - web: - environment: - - RANDOM_MODES=1 - - RANDOM_UI=1 - - RANDOM_MAX_ATTEMPTS=5 - - RANDOM_TIMEOUT_MS=5000 -``` - -For deterministic tests or development, you can point the app to a frozen dataset snapshot: - -```yaml -services: - web: - environment: - - CSV_FILES_DIR=/app/csv_files/testdata -``` - -### Taxonomy snapshot (maintainers) -Capture the current bracket taxonomy into an auditable JSON file inside the container: +- Stop or restart the service when you're done: ```powershell -docker compose run --rm web bash -lc "python -m code.scripts.snapshot_taxonomy" +docker compose stop web +docker compose start web ``` -Artifacts appear under `./logs/taxonomy_snapshots/` on your host via the mounted volume. -To force a new snapshot even when the content hash matches the latest, pass `--force` to the module. +- Prefer the public image? Pull and run it without building locally: -## Volumes -- `/app/deck_files` ↔ `./deck_files` -- `/app/logs` ↔ `./logs` -- `/app/csv_files` ↔ `./csv_files` -- `/app/owned_cards` ↔ `./owned_cards` (owned cards lists: .txt/.csv) -- Optional: `/app/config` ↔ `./config` (JSON configs for headless) - -## Interactive vs headless -- Interactive: attach a TTY (compose run or `docker run -it`) -- Headless auto-run: - ```powershell - docker compose run --rm -e DECK_MODE=headless mtg-deckbuilder - ``` -- Headless with JSON config: - ```powershell - docker compose run --rm ` - -e DECK_MODE=headless ` - -e DECK_CONFIG=/app/config/deck.json ` - mtg-deckbuilder - ``` - -### Common env vars -- DECK_MODE=headless -- DECK_CONFIG=/app/config/deck.json -- DECK_COMMANDER, DECK_PRIMARY_CHOICE -- DECK_ADD_LANDS, DECK_FETCH_COUNT - - DECK_TAG_MODE=AND|OR (combine mode used by the builder) - -### Web UI tuning env vars -- WEB_TAG_PARALLEL=1|0 (parallel tagging on/off) -- WEB_TAG_WORKERS= (process count; set based on CPU/memory) -- WEB_VIRTUALIZE=1 (enable virtualization) -- SHOW_DIAGNOSTICS=1 (enables diagnostics pages and overlay hotkey `v`) -- RANDOM_MODES=1 (enable random build endpoints) -- RANDOM_UI=1 (show Surprise/Theme/Reroll/Share controls) -- RANDOM_MAX_ATTEMPTS=5 (cap retry attempts) -- (Upcoming) Multi-theme inputs: once UI ships, Random Mode will accept `primary_theme`, `secondary_theme`, `tertiary_theme` fields; current backend already supports the cascade + diagnostics. -- RANDOM_TIMEOUT_MS=5000 (per-build timeout in ms) - -Testing/determinism helper (dev): -- CSV_FILES_DIR=csv_files/testdata — override CSV base dir to a frozen set for tests - -## Manual build/run ```powershell -docker build -t mtg-deckbuilder . -docker run -it --rm ` - -v "${PWD}/deck_files:/app/deck_files" ` - -v "${PWD}/logs:/app/logs" ` - -v "${PWD}/csv_files:/app/csv_files" ` - -v "${PWD}/owned_cards:/app/owned_cards" ` - -v "${PWD}/config:/app/config" ` - mtg-deckbuilder +docker run --rm -p 8080:8080 ` + -v "${PWD}/deck_files:/app/deck_files" ` + -v "${PWD}/logs:/app/logs" ` + -v "${PWD}/csv_files:/app/csv_files" ` + -v "${PWD}/config:/app/config" ` + -v "${PWD}/owned_cards:/app/owned_cards" ` + mwisnowski/mtg-python-deckbuilder:latest ``` +Shared volumes persist builds, logs, configs, and owned cards on the host. + +## Run a JSON Config + +Use the homepage “Run a JSON Config” button or run the same flow in-container: + +```powershell +docker compose run --rm ` + -e APP_MODE=cli ` + -e DECK_MODE=headless ` + -e DECK_CONFIG=/app/config/deck.json ` + web +``` + +- `APP_MODE=cli` routes the entrypoint to the CLI menu. +- `DECK_MODE=headless` skips prompts and calls `headless_runner`. +- Mount JSON configs under `config/` so both the UI and CLI can pick them up. + +Override counts, theme tags, or include/exclude lists by setting the matching environment variables before running the container (see “Environment variables” below). + +## Initial Setup + +The homepage “Initial Setup” tile appears when `SHOW_SETUP=1` (enabled in compose). It re-runs: + +1. Card downloads and color-filtered CSV generation. +2. Commander catalog rebuild (including multi-face merges). +3. Tagging and caching. + +To force a rebuild from the host: + +```powershell +docker compose run --rm --entrypoint bash web -lc "python -m code.file_setup.setup" +``` + +Add `--entrypoint bash ... "python -m code.scripts.refresh_commander_catalog"` when you only need the commander catalog (with MDFC merge and optional compatibility snapshot). + +## Owned Library + +Store `.txt` or `.csv` lists in `owned_cards/` (mounted to `/app/owned_cards`). The Web UI uses them for: + +- Owned-only or prefer-owned builds. +- The Owned Library management page (virtualized when `WEB_VIRTUALIZE=1`). +- Alternative suggestions that respect ownership. + +Use `/owned` to upload files and export enriched lists. These files persist through the `owned_cards` volume. + +## Browse Commanders + +`SHOW_COMMANDERS=1` exposes the commander browser tile. + +- Data lives in `csv_files/commander_cards.csv`. +- Refresh the catalog (including MDFC merges) from within the container: + +```powershell +docker compose run --rm --entrypoint bash web -lc "python -m code.scripts.refresh_commander_catalog" +``` + +Pass `--compat-snapshot` if you also need an unmerged compatibility CSV under `csv_files/compat_faces/`. + +## Finished Decks + +The Finished Decks page reads the `deck_files/` volume for completed builds: + +- Each run produces CSV, TXT, compliance JSON, and summary JSON sidecars. +- Locks and replace history persist per deck. +- Compare view can diff and export summaries. + +Ensure the deck exports volume remains mounted so these artifacts survive container restarts. + +## Browse Themes + +The Themes browser exposes the merged theme catalog with search, filters, and diagnostics. + +- `ENABLE_THEMES=1` keeps the selector visible. +- `WEB_THEME_PICKER_DIAGNOSTICS=1` unlocks uncapped synergies, extra metadata, and `/themes/metrics`. +- Regenerate the catalog manually: + +```powershell +docker compose run --rm --entrypoint bash web -lc "python -m code.scripts.build_theme_catalog" +``` + +Advanced options (e.g., `EDITORIAL_*` variables) live in `.env.example`. + +## Random Build + +Enable the Surprise/Reroll flow by setting: + +- `RANDOM_MODES=1` to expose backend random endpoints. +- `RANDOM_UI=1` to show the Random Build tile. +- Optional tunables: `RANDOM_MAX_ATTEMPTS`, `RANDOM_TIMEOUT_MS`, `RANDOM_PRIMARY_THEME`, `RANDOM_SEED`, and auto-fill flags. + +Headless parity is available by pairing `APP_MODE=cli` with `DECK_MODE=headless` and the same random variables. + +## Diagnostics + +`SHOW_DIAGNOSTICS=1` unlocks `/diagnostics` for system summaries, feature flags, and performance probes. Highlights: + +- `/healthz` returns `{status, version, uptime_seconds}` for external monitoring. +- Press `v` on pages with virtualized grids (when `WEB_VIRTUALIZE=1`) to toggle the range overlay. +- `WEB_AUTO_ENFORCE=1` (optional) applies bracket enforcement automatically after each build. + +## View Logs + +`SHOW_LOGS=1` enables the logs tile and `/logs` interface: + +- Tail the container log with filtering and copy-to-clipboard. +- `/status/logs?tail=200` offers a lightweight JSON endpoint. +- Raw files live under `logs/` on the host; rotate or archive them as needed. + +## Environment variables (Docker quick reference) + +See `.env.example` for the full catalog. Common knobs: + +### Core mode and networking + +| Variable | Default | Purpose | +| --- | --- | --- | +| `APP_MODE` | `web` | Switch between Web UI (`web`) and CLI (`cli`). | +| `DECK_MODE` | _(unset)_ | `headless` auto-runs the headless builder when the CLI starts. | +| `DECK_CONFIG` | `/app/config/deck.json` | JSON config file or directory (auto-discovery). | +| `HOST` / `PORT` / `WORKERS` | `0.0.0.0` / `8080` / `1` | Uvicorn binding when `APP_MODE=web`. | + +### Homepage visibility & UX + +| Variable | Default | Purpose | +| --- | --- | --- | +| `SHOW_SETUP` | `1` | Show the Initial Setup card. | +| `SHOW_LOGS` | `1` | Enable the View Logs tile and endpoints. | +| `SHOW_DIAGNOSTICS` | `1` | Enable Diagnostics tools and overlays. | +| `SHOW_COMMANDERS` | `1` | Expose the commander browser. | +| `ENABLE_THEMES` | `1` | Keep the theme selector and themes explorer visible. | +| `WEB_VIRTUALIZE` | `1` | Opt-in to virtualized lists/grids for large result sets. | +| `ALLOW_MUST_HAVES` | `1` | Enable include/exclude enforcement in Step 5. | +| `THEME` | `dark` | Initial UI theme (`system`, `light`, or `dark`). | + +### Random build controls + +| Variable | Default | Purpose | +| --- | --- | --- | +| `RANDOM_MODES` | _(unset)_ | Enable random build endpoints. | +| `RANDOM_UI` | _(unset)_ | Show the Random Build homepage tile. | +| `RANDOM_MAX_ATTEMPTS` | `5` | Retry budget for constrained random rolls. | +| `RANDOM_TIMEOUT_MS` | `5000` | Per-attempt timeout in milliseconds. | +| `RANDOM_PRIMARY_THEME` / `RANDOM_SECONDARY_THEME` / `RANDOM_TERTIARY_THEME` | _(blank)_ | Override theme slots for random runs. | +| `RANDOM_SEED` | _(blank)_ | Deterministic seed. | +| `RANDOM_AUTO_FILL` | `1` | Allow automatic backfill of missing theme slots. | + +### Automation & performance + +| Variable | Default | Purpose | +| --- | --- | --- | +| `WEB_AUTO_SETUP` | `1` | Auto-run data setup when artifacts are missing or stale. | +| `WEB_AUTO_REFRESH_DAYS` | `7` | Refresh `cards.csv` if older than N days. | +| `WEB_TAG_PARALLEL` | `1` | Use parallel workers during tagging. | +| `WEB_TAG_WORKERS` | `4` | Worker count for parallel tagging. | +| `WEB_AUTO_ENFORCE` | `0` | Re-export decks after auto-applying compliance fixes. | +| `WEB_THEME_PICKER_DIAGNOSTICS` | `1` | Enable theme diagnostics endpoints. | + +### Paths and data overrides + +| Variable | Default | Purpose | +| --- | --- | --- | +| `CSV_FILES_DIR` | `/app/csv_files` | Point the app at an alternate dataset (e.g., test snapshots). | +| `DECK_EXPORTS` | `/app/deck_files` | Override where the web UI looks for exports. | +| `OWNED_CARDS_DIR` / `CARD_LIBRARY_DIR` | `/app/owned_cards` | Override owned library directory. | +| `CARD_INDEX_EXTRA_CSV` | _(blank)_ | Inject a synthetic CSV into the card index for testing. | + +Advanced editorial and theme-catalog knobs (`EDITORIAL_*`, `SPLASH_ADAPTIVE`, etc.) are documented inline in `docker-compose.yml` and `.env.example`. + +## Shared volumes + +| Host path | Container path | Contents | +| --- | --- | --- | +| `deck_files/` | `/app/deck_files` | CSV/TXT exports, summary JSON, compliance reports. | +| `logs/` | `/app/logs` | Application logs and taxonomy snapshots. | +| `csv_files/` | `/app/csv_files` | Card datasets, commander catalog, tagging flags. | +| `config/` | `/app/config` | JSON configs, bracket policy, card list overrides. | +| `owned_cards/` | `/app/owned_cards` | Uploaded inventory files for owned-only flows. | + +## Maintenance commands + +Run ad-hoc tasks by overriding the entrypoint: + +```powershell +# Theme catalog rebuild +docker compose run --rm --entrypoint bash web -lc "python -m code.scripts.build_theme_catalog" + +# Snapshot taxonomy (writes logs/taxonomy_snapshots/) +docker compose run --rm --entrypoint bash web -lc "python -m code.scripts.snapshot_taxonomy" + +# Preview the MDFC commander diff +docker compose run --rm --entrypoint bash web -lc "python -m code.scripts.preview_dfc_catalog_diff" +``` + +Use the `--compat-snapshot` or other script arguments as needed. + ## Troubleshooting -- No prompts? Use `docker compose run --rm` (not `up`) or add `-it` to `docker run` -- Files not saving? Verify volume mounts and that folders exist -- Headless not picking config? Ensure `./config` is mounted to `/app/config` and `DECK_CONFIG` points to a JSON file -- Owned-cards prompt not seeing files? Ensure `./owned_cards` is mounted to `/app/owned_cards` -## Tips -- Use `docker compose run`, not `up`, for interactive mode -- Exported decks appear in `deck_files/` -- JSON run-config is exported only in interactive runs; headless skips it +- **Container starts but UI stays blank:** check `/healthz` and `/logs` (enable with `SHOW_LOGS=1`), then inspect the `logs/` volume. +- **Files missing on the host:** ensure the host directories exist before starting Compose; Windows will create empty folders if the path is invalid. +- **Long first boot:** dataset downloads and tagging can take several minutes the first time. Watch progress at `/setup`. +- **Random build hangs:** lower `RANDOM_MAX_ATTEMPTS` or raise `RANDOM_TIMEOUT_MS`, and confirm your theme overrides are valid slugs via `/themes/`. +- **Commander catalog outdated:** rerun the refresh command above or delete `csv_files/.tagging_complete.json` to force a full rebuild on next start. diff --git a/README.md b/README.md index 6024b28e8f6315add47a7f182fe3b3818871e640..112d7f44c4ed574cac0320ecbc2d9d03bd8b95a8 100644 GIT binary patch literal 29874 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 diff --git a/docs/archive/DOCKER_2025-10-02.md b/docs/archive/DOCKER_2025-10-02.md new file mode 100644 index 0000000..98ea6cc --- /dev/null +++ b/docs/archive/DOCKER_2025-10-02.md @@ -0,0 +1,228 @@ +# Docker Guide + +Run the MTG Deckbuilder (CLI and Web UI) in Docker with persistent volumes and optional headless mode. + +## Quick start + +### PowerShell (recommended) +```powershell +docker compose build +docker compose run --rm mtg-deckbuilder +``` + +### From Docker Hub (PowerShell) +```powershell +docker run -it --rm ` + -v "${PWD}/deck_files:/app/deck_files" ` + -v "${PWD}/logs:/app/logs" ` + -v "${PWD}/csv_files:/app/csv_files" ` + -v "${PWD}/owned_cards:/app/owned_cards" ` + -v "${PWD}/config:/app/config" ` + mwisnowski/mtg-python-deckbuilder:latest +``` + +## Web UI (new) + +The web UI runs the same deckbuilding logic behind a browser-based interface. + +### PowerShell (recommended) +```powershell +docker compose up --build --no-deps -d web +``` + +Then open http://localhost:8080 + +Volumes are the same as the CLI service, so deck exports/logs/configs persist in your working folder. +The app serves a favicon at `/favicon.ico` and exposes a health endpoint at `/healthz`. +Compare view offers a Copy summary button to copy a plain-text diff of two runs. The sidebar has a subtle depth shadow for clearer separation. + +Web UI feature highlights: +- Locks: Click a card or the lock control in Step 5; locks persist across reruns. +- Replace: Enable Replace in Step 5, click a card to open Alternatives (filters include Owned-only), then choose a swap. +- Permalinks: Copy a permalink from Step 5 or a Finished deck; paste via “Open Permalink…” to restore. +- Compare: Use the Compare page from Finished Decks; quick actions include Latest two and Swap A/B. + +Virtualized lists and lazy images (opt‑in) +- Set `WEB_VIRTUALIZE=1` to enable virtualization in Step 5 grids/lists and the Owned library for smoother scrolling on large sets. +- Example (Compose): + ```yaml + services: + web: + environment: + - WEB_VIRTUALIZE=1 + ``` +- Example (Docker Hub): + ```powershell + docker run --rm -p 8080:8080 ` + -e WEB_VIRTUALIZE=1 ` + -v "${PWD}/deck_files:/app/deck_files" ` + -v "${PWD}/logs:/app/logs" ` + -v "${PWD}/csv_files:/app/csv_files" ` + -v "${PWD}/owned_cards:/app/owned_cards" ` + -v "${PWD}/config:/app/config" ` + -e SHOW_DIAGNOSTICS=1 ` # optional: enables diagnostics tools and overlay + mwisnowski/mtg-python-deckbuilder:latest ` + bash -lc "cd /app && uvicorn code.web.app:app --host 0.0.0.0 --port 8080" + ``` + +### Diagnostics and logs (optional) +Enable internal diagnostics and a read-only logs viewer with environment flags. + +- `SHOW_DIAGNOSTICS=1` — adds a Diagnostics nav link and `/diagnostics` tools +- `SHOW_LOGS=1` — enables `/logs` and `/status/logs?tail=200` + +When enabled: +- `/logs` supports an auto-refresh toggle with interval, a level filter (All/Error/Warning/Info/Debug), and a Copy button to copy the visible tail. +- `/status/sys` returns a simple system summary (version, uptime, UTC server time, and feature flags) and is shown on the Diagnostics page when `SHOW_DIAGNOSTICS=1`. + - Virtualization overlay: press `v` on pages with virtualized grids to toggle per-grid overlays and a global summary bubble. + +Compose example (web service): +```yaml +environment: + - SHOW_LOGS=1 + - SHOW_DIAGNOSTICS=1 +``` + +Docker Hub (PowerShell) example: +```powershell +docker run --rm ` + -p 8080:8080 ` + -e SHOW_LOGS=1 -e SHOW_DIAGNOSTICS=1 -e ENABLE_THEMES=1 -e THEME=system ` + -e SPLASH_ADAPTIVE=1 -e SPLASH_ADAPTIVE_SCALE="1:1.0,2:1.0,3:1.0,4:0.6,5:0.35" ` # optional experiment + -e RANDOM_MODES=1 -e RANDOM_UI=1 -e RANDOM_MAX_ATTEMPTS=5 -e RANDOM_TIMEOUT_MS=5000 ` + -v "${PWD}/deck_files:/app/deck_files" ` + -v "${PWD}/logs:/app/logs" ` + -v "${PWD}/csv_files:/app/csv_files" ` + -v "${PWD}/owned_cards:/app/owned_cards" ` + -v "${PWD}/config:/app/config" ` + mwisnowski/mtg-python-deckbuilder:latest ` + bash -lc "cd /app && uvicorn code.web.app:app --host 0.0.0.0 --port 8080" +``` + +### Setup speed: parallel tagging (Web) +First-time setup or stale data triggers card tagging. The web service uses parallel workers by default. + +Configure via environment variables on the `web` service: +- `WEB_TAG_PARALLEL=1|0` — enable/disable parallel tagging (default: 1) +- `WEB_TAG_WORKERS=` — number of worker processes (default: 4 in compose) + +If parallel initialization fails, the service falls back to sequential tagging and continues. + +### From Docker Hub (PowerShell) +If you prefer not to build locally, pull `mwisnowski/mtg-python-deckbuilder:latest` and run uvicorn: +```powershell +docker run --rm ` + -p 8080:8080 ` + -v "${PWD}/deck_files:/app/deck_files" ` + -v "${PWD}/logs:/app/logs" ` + -v "${PWD}/csv_files:/app/csv_files" ` + -v "${PWD}/owned_cards:/app/owned_cards" ` + -v "${PWD}/config:/app/config" ` + mwisnowski/mtg-python-deckbuilder:latest ` + bash -lc "cd /app && uvicorn code.web.app:app --host 0.0.0.0 --port 8080" +``` + +Health check: +```text +GET http://localhost:8080/healthz -> { "status": "ok", "version": "dev", "uptime_seconds": 123 } +``` + +Theme preference reset (client-side): use the header’s Reset Theme control to clear the saved browser preference; the server default (THEME) applies on next paint. + +### Random Modes (alpha) and test dataset override + +Enable experimental Random Modes and UI controls in Web runs by setting: + +```yaml +services: + web: + environment: + - RANDOM_MODES=1 + - RANDOM_UI=1 + - RANDOM_MAX_ATTEMPTS=5 + - RANDOM_TIMEOUT_MS=5000 +``` + +For deterministic tests or development, you can point the app to a frozen dataset snapshot: + +```yaml +services: + web: + environment: + - CSV_FILES_DIR=/app/csv_files/testdata +``` + +### Taxonomy snapshot (maintainers) +Capture the current bracket taxonomy into an auditable JSON file inside the container: + +```powershell +docker compose run --rm web bash -lc "python -m code.scripts.snapshot_taxonomy" +``` +Artifacts appear under `./logs/taxonomy_snapshots/` on your host via the mounted volume. + +To force a new snapshot even when the content hash matches the latest, pass `--force` to the module. + +## Volumes +- `/app/deck_files` ↔ `./deck_files` +- `/app/logs` ↔ `./logs` +- `/app/csv_files` ↔ `./csv_files` +- `/app/owned_cards` ↔ `./owned_cards` (owned cards lists: .txt/.csv) +- Optional: `/app/config` ↔ `./config` (JSON configs for headless) + +## Interactive vs headless +- Interactive: attach a TTY (compose run or `docker run -it`) +- Headless auto-run: + ```powershell + docker compose run --rm -e DECK_MODE=headless mtg-deckbuilder + ``` +- Headless with JSON config: + ```powershell + docker compose run --rm ` + -e DECK_MODE=headless ` + -e DECK_CONFIG=/app/config/deck.json ` + mtg-deckbuilder + ``` + +### Common env vars +- DECK_MODE=headless +- DECK_CONFIG=/app/config/deck.json +- DECK_COMMANDER, DECK_PRIMARY_CHOICE +- DECK_ADD_LANDS, DECK_FETCH_COUNT + - DECK_TAG_MODE=AND|OR (combine mode used by the builder) + +### Web UI tuning env vars +- WEB_TAG_PARALLEL=1|0 (parallel tagging on/off) +- WEB_TAG_WORKERS= (process count; set based on CPU/memory) +- WEB_VIRTUALIZE=1 (enable virtualization) +- SHOW_DIAGNOSTICS=1 (enables diagnostics pages and overlay hotkey `v`) +- RANDOM_MODES=1 (enable random build endpoints) +- RANDOM_UI=1 (show Surprise/Theme/Reroll/Share controls) +- RANDOM_MAX_ATTEMPTS=5 (cap retry attempts) +- (Upcoming) Multi-theme inputs: once UI ships, Random Mode will accept `primary_theme`, `secondary_theme`, `tertiary_theme` fields; current backend already supports the cascade + diagnostics. +- RANDOM_TIMEOUT_MS=5000 (per-build timeout in ms) + +Testing/determinism helper (dev): +- CSV_FILES_DIR=csv_files/testdata — override CSV base dir to a frozen set for tests + +## Manual build/run +```powershell +docker build -t mtg-deckbuilder . +docker run -it --rm ` + -v "${PWD}/deck_files:/app/deck_files" ` + -v "${PWD}/logs:/app/logs" ` + -v "${PWD}/csv_files:/app/csv_files" ` + -v "${PWD}/owned_cards:/app/owned_cards" ` + -v "${PWD}/config:/app/config" ` + mtg-deckbuilder +``` + +## Troubleshooting +- No prompts? Use `docker compose run --rm` (not `up`) or add `-it` to `docker run` +- Files not saving? Verify volume mounts and that folders exist +- Headless not picking config? Ensure `./config` is mounted to `/app/config` and `DECK_CONFIG` points to a JSON file +- Owned-cards prompt not seeing files? Ensure `./owned_cards` is mounted to `/app/owned_cards` + +## Tips +- Use `docker compose run`, not `up`, for interactive mode +- Exported decks appear in `deck_files/` +- JSON run-config is exported only in interactive runs; headless skips it diff --git a/docs/archive/README_2025-10-02.md b/docs/archive/README_2025-10-02.md new file mode 100644 index 0000000..f65426e --- /dev/null +++ b/docs/archive/README_2025-10-02.md @@ -0,0 +1,951 @@ +# 🃏 MTG Python Deckbuilder + +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/) +[![Docker](https://img.shields.io/badge/docker-supported-blue.svg)](https://www.docker.com/) + +A fast, menu-driven deckbuilder for MTG Commander/EDH with both CLI and Web UI. Supports interactive and headless runs, CSV/TXT exports, owned-card libraries, and Docker. + +## 🔹 Features +- Web UI and CLI, powered by the same builder +- Commander search with smart theme tagging (primary/secondary/tertiary) +- Power bracket selection and ideal count prompts +- **Include/Exclude Card Lists**: Must-include and must-exclude functionality with fuzzy matching, visual validation, and strict enforcement (set `ALLOW_MUST_HAVES=true`) +- CSV and TXT exports with stable filenames +- Headless mode (non-interactive) and a headless submenu in the main menu +- Config precedence: CLI > env > JSON > defaults + - Visual summaries in Web UI: Mana curve, Color pips and Sources with hover-to-highlight and copyable tooltips +- Web UI: Locks, Replace flow, Compare builds, and shareable permalinks + - Unified “New Deck” modal (steps 1–3): commander search, themes, bracket, options, and optional Name (used in export filenames) + - Multi-copy archetypes (opt-in modal): choose quantity for supported archetypes and optionally add Thrumming Stone; added early with target adjustments and a hard 100-card clamp + - Combos & Synergies: detect curated pairs, show chip-style lists with badges, and dual-card hover previews; optional auto-complete adds missing partners based on your plan. + +## ✨ Highlights +- Smart tagging and suggestions for commander + themes, with AND/OR combine modes +- Theme governance: generated theme catalog with whitelist normalization, enforced synergies, and capped synergy lists (top 5) for concise UI; roadmap includes YAML authoring export. +- Exports CSV and TXT decklists to `deck_files/` +- Random Full Build (alpha) now outputs a single authoritative artifact set (CSV, TXT, compliance JSON, summary JSON) without duplicate suffixed files; API returns paths for integration. Summary sidecars carry multi-theme metadata (`primary_theme`, `secondary_theme`, `tertiary_theme`, `resolved_themes`, fallback flags) plus legacy aliases for incremental consumer upgrades. +- Random Mode (alpha) multi-theme groundwork: curated pools now support manual exclusions via `config/random_theme_exclusions.yml` (documented in `docs/random_theme_exclusions.md` and exportable with `python code/scripts/report_random_theme_pool.py --write-exclusions`); UI/diagnostics surface fallback telemetry while the upcoming picker adds Primary/Secondary/Tertiary inputs with deterministic fallback (P+S+T → P+S → P+T → P → synergy overlap → full pool) and an inline “Clear themes” control quickly resets stored inputs. +- Random Mode instrumentation: `python code/scripts/profile_multi_theme_filter.py` captures mean/p95 filter timings, while `python code/scripts/check_random_theme_perf.py` guards against regressions relative to `config/random_theme_perf_baseline.json` (`--update-baseline` refreshes). +- Owned-cards mode: point to one or more `.txt`/`.csv` files in `owned_cards/` and choose "Use only owned cards?" +- If you opt out, final CSV marks owned cards with an `Owned` column +- Build options include Prefer-owned: bias picks toward owned cards while allowing unowned fallback +- Interactive menu with a headless submenu +- Works locally or in Docker (Windows PowerShell friendly) +- Card images and data via Scryfall (attribution shown in the Web UI) + - Web visual summaries: cross-highlight cards from charts; sources include non-lands and colorless 'C' with an on/off toggle + - New Deck modal keyboard UX: Enter selects the first suggestion; arrow navigation removed; browser autofill disabled + - Export naming: optional Name in the modal becomes the filename stem for CSV/TXT/JSON; decks include a sidecar `.summary.json` with `meta.name` + - Finished Decks and deck banners prefer your custom Name when present + - Compare page includes a Copy summary button to quickly share diffs + - New Deck modal shows your selected themes and their order (1 → 3) while picking; Bracket options are labeled (e.g., "Bracket 3: Upgraded"). Default bracket is 3. + - Bracket policy enforcement: disallowed categories (e.g., Game Changers in Brackets 1–2) are pruned from the pool up-front; UI blocks progression until violations are resolved, with inline Replace to pick compliant alternatives. + - Bracket compliance UX: the compliance panel auto-opens when non-compliant and shows a colored overall chip (green/WARN amber/red). WARN thresholds (e.g., tutors/extra turns) are advisory—tiles show with a subtle style and no gating; FAIL continues to gate and enables enforcement. + +### Commander eligibility rules (clarified) +The builder now applies stricter Commander legality filtering when generating `commander_cards.csv`: +1. Automatically eligible: + - Legendary Creatures (includes Artifact Creature / Enchantment Creature lines) + - Legendary Artifact Vehicles or Spacecraft that have printed power and toughness + - Any card whose rules text explicitly contains "can be your commander" +2. Not auto‑eligible (unless they have the explicit text above): + - Plain Legendary Planeswalkers + - Plain Legendary Enchantments without the Creature type + - Generic Legendary Artifacts (non‑Vehicle/Spacecraft or without P/T) +This prevents over-broad inclusion of non-creature legendary permanents. + +## 🚀 Quick start + +### Docker Hub (PowerShell) +```powershell +docker run -it --rm ` + -v "${PWD}/deck_files:/app/deck_files" ` + -v "${PWD}/logs:/app/logs" ` + -v "${PWD}/csv_files:/app/csv_files" ` + -v "${PWD}/owned_cards:/app/owned_cards" ` + -v "${PWD}/config:/app/config" ` + mwisnowski/mtg-python-deckbuilder:latest +``` + +### From source +```powershell +pip install -r requirements.txt +python code/main.py +``` + +### From this repo with Docker Compose (PowerShell) +```powershell +docker compose build +docker compose run --rm mtg-deckbuilder +``` + +## 🌐 Web UI + +Run the browser-based UI backed by the same deckbuilding engine. + +### From source +```powershell +pip install -r requirements.txt +uvicorn code.web.app:app --host 127.0.0.1 --port 8080 +``` + +Open http://127.0.0.1:8080 + +### Docker Compose (PowerShell) +```powershell +docker compose build web +docker compose up --no-deps web +``` + +Then open http://localhost:8080 + +Notes: +- Exports/logs/configs use the same folders and will appear under your mounted volumes (see compose file). +- The footer includes Scryfall attribution per their guidelines. + - Favicon is bundled; browsers load `/favicon.ico` (auto-falls back to PNG). + - Bracket enforcement is applied in both web and headless runs; see "Bracket policy & enforcement" below. + +### Run with Docker Compose (Docker Hub image) + +Use the prebuilt image with the provided `dockerhub-docker-compose.yml`. The image defaults to the Web UI. + +Web (default): +```powershell +docker compose -f dockerhub-docker-compose.yml up -d +``` + +Pin to a version (edit the compose file image tag to `:X.Y.Z`) to avoid `latest` drift. + +CLI mode (override default): +```powershell +docker compose -f dockerhub-docker-compose.yml run --rm ` + -e APP_MODE=cli ` + web +``` + +Notes: +- Volumes under `${PWD}` persist your exports/logs/configs locally next to the compose file. +- Health checks are built into the image for the Web UI. In CLI, `APP_MODE=cli` bypasses health. + +#### Theme defaults and flags +- Enable the Theme selector with `ENABLE_THEMES=1` (on by default in `web` in compose). +- Set the initial default with `THEME=system|light|dark` (applies only when the browser has no prior preference saved): + - `THEME=system` (recommended): follows the OS theme; Light uses the consolidated Light (Blend) palette. + - `THEME=light`: maps to the consolidated Light (Blend) theme in the UI. + - `THEME=dark`: uses the dark theme. +- You can also force a one-off override via URL: `?theme=system|light|dark|high-contrast|cb-friendly`. + - Example: `http://localhost:8080/?theme=dark` + - The choice is saved in localStorage; the URL parameter is removed after applying. + - Reset: Use the “Reset theme” control in the header to clear your preference and re‑apply the server default (or system). + - YAML export strategy: By default the app now always regenerates per-theme YAML files even on fast refreshes to keep editorial artifacts synchronized with `theme_list.json`. To opt-out on incremental refreshes (rare), set `THEME_YAML_FAST_SKIP=1` (compose variable). This only skips export during fast-path (no retag) theme refreshes and never during full builds. + +#### Theme catalog editorial pipeline +Build script: `python code/scripts/build_theme_catalog.py` + +Key flags / env vars: +- `--limit N` (preview subset; guarded from overwriting canonical JSON unless `--allow-limit-write`) +- `--output path` (write catalog to alternate location; suppresses YAML backfill so tests don't mutate source files) +- `--backfill-yaml` or `EDITORIAL_BACKFILL_YAML=1` (write auto `description` + derived/pinned `popularity_bucket` into each YAML if missing) +- `--force-backfill-yaml` (overwrite existing description/popularity_bucket) +- `EDITORIAL_SEED=` (deterministic ordering for inference and any randomized fallbacks) +- `EDITORIAL_AGGRESSIVE_FILL=1` (pad ultra-sparse themes with extra inferred synergies) +- `EDITORIAL_POP_BOUNDARIES="a,b,c,d"` (override popularity bucket thresholds) +- `EDITORIAL_POP_EXPORT=1` (emit `theme_popularity_metrics.json` summary) + +Derived fields: +- `popularity_bucket` (Very Common / Common / Uncommon / Niche / Rare) based on summed color frequency; authors may pin a value in YAML. +- `description` auto-generated if absent using heuristics keyed to theme class (tribal, counters, graveyard, tokens, etc.). + +Editorial quality & metadata info: +- Optional `editorial_quality: draft|reviewed|final` can be set in per-theme YAML (or force-backfilled) to track curation progress. +- Backfills now stamp a `metadata_info` block (formerly `provenance`) with keys such as `last_backfill`, `script`, and `version`. The legacy key `provenance` is still parsed for a short deprecation window; if both appear a warning is emitted (suppress via `SUPPRESS_PROVENANCE_DEPRECATION=1`). Planned removal of the legacy alias: version 2.4.0. +- A running specialization trend is appended to `config/themes/description_fallback_history.jsonl` whenever `EDITORIAL_INCLUDE_FALLBACK_SUMMARY=1` is set. +- When `EDITORIAL_INCLUDE_FALLBACK_SUMMARY=1`, the catalog additionally embeds a `description_fallback_summary` object exposing coverage KPIs (see below) to power regression tests. + +Synergy pairs & clusters: +- `config/themes/synergy_pairs.yml` provides curated fallback synergies (applied only if a theme YAML lacks `curated_synergies`). +- `config/themes/theme_clusters.yml` groups themes (Tokens, Counters, Graveyard, etc.) for upcoming UI filters and analytics. + - Validator checks structure; UI integration planned in Phase E. + +Description mapping externalization: +- `config/themes/description_mapping.yml` fully replaces internal mapping when present. + - Items: `triggers` (list of substrings, lowercase-insensitive) and `description` string. + - `{SYNERGIES}` placeholder expands to a short clause with top synergy names or is removed gracefully if none. + - Mapping validator (`scripts/validate_description_mapping.py`) reports duplicate triggers, suggests `{SYNERGIES}` usage, and validates synergy pairs & clusters. + +KPI targets (initial schedule): +- Wave 1 (current): Maintain regression test thresholds (generic_pct < 52%). +- Wave 2: Ratchet to <45% after next mapping expansion. +- Wave 3: <38%; Wave 4: <30% (issue templates will adjust thresholds once prior target is achieved and history confirms downward trend). + +Lint (`code/scripts/lint_theme_editorial.py`) enhancements: +- `--require-description` / env `EDITORIAL_REQUIRE_DESCRIPTION=1` enforces descriptions present. +- `--require-popularity` / env `EDITORIAL_REQUIRE_POPULARITY=1` enforces popularity bucket present. +- Strict mode also escalates missing cornerstone examples and missing description/popularity to errors. + +Testing: +Determinism, boundary overrides, and backfill isolation validated in `code/tests/test_theme_catalog_generation.py` using the new `--output` flag. + +### Commander catalog upkeep +- The Commander Browser and auto-select flows read from `csv_files/commander_cards.csv`; keep it version-controlled so web and CLI stay in sync. +- Regenerate the file after MTGJSON updates or commander policy tweaks: + ```powershell + python -c "from file_setup.setup import regenerate_csvs_all; regenerate_csvs_all()" + ``` +- For quick fixes, you can refresh just the commander list: + ```powershell + python -c "from file_setup.setup import determine_commanders; determine_commanders()" + ``` +- Full onboarding guidance (required columns, caching notes, validation steps) lives in `docs/commander_catalog.md`. +- Staging toggle: set `SHOW_COMMANDERS=0` to hide the sidebar link while testing privately; defaults to on. + +### Theme Catalog API +Two new endpoints expose the merged catalog for the upcoming theme picker UI. + +List themes: +`GET /themes/api/themes` +Parameters: +- `q` substring across theme name & synergies +- `archetype` filter by `deck_archetype` +- `bucket` popularity bucket (Very Common|Common|Uncommon|Niche|Rare) +- `colors` comma-separated color initials (e.g. `G,W`) +- `limit` (default 50, max 200) & `offset` +- `diagnostics=1` (requires env `WEB_THEME_PICKER_DIAGNOSTICS=1`) to expose `has_fallback_description` + `editorial_quality` + +Sample response snippet: +``` +{ + "ok": true, + "count": 314, + "items": [ + { + "id": "plus1-plus1-counters", + "theme": "+1/+1 Counters", + "primary_color": "Green", + "secondary_color": "White", + "popularity_bucket": "Very Common", + "deck_archetype": "Counters", + "description": "...", + "synergies": ["Adapt","Evolve","Proliferate","Counters Matter","Hydra Kindred"], + "synergy_count": 5 + // diagnostics-only: has_fallback_description, editorial_quality + } + ], + "next_offset": 50, + "generated_at": "2025-09-19T12:34:56", + "diagnostics": false +} +``` + +Theme detail: +`GET /themes/api/theme/{id}` +Parameters: +- `uncapped=1` (diagnostics only) returns `uncapped_synergies` (full curated+enforced+inferred ordered set) +- `diagnostics=1` enables extra fields (fallback + editorial quality) + +Detail response excerpt: +``` +{ + "ok": true, + "theme": { + "id": "plus1-plus1-counters", + "theme": "+1/+1 Counters", + "description": "...", + "synergies": ["Adapt","Evolve","Proliferate","Counters Matter","Hydra Kindred"], + "curated_synergies": [...], + "enforced_synergies": [...], + "inferred_synergies": [...], + "example_commanders": [...], + "example_cards": [...], + "synergy_commanders": [...] + // diagnostics-only: uncapped_synergies, editorial_quality, has_fallback_description + } +} +``` + +Caching & freshness: ETag header reflects catalog size/mtime + newest YAML mtime. Use `/themes/status` for stale detection; POST `/themes/refresh` to rebuild in background. + +Environment flag: `WEB_THEME_PICKER_DIAGNOSTICS=1` enables diagnostics extras and uncapped synergy toggle. + +Performance & optimization: +- Fast list filtering uses precomputed lowercase search haystacks + memoized filtered slug cache (keyed by catalog ETag and query params) for sub‑50ms typical responses once warm. +- Skeleton loaders (picker shell, table list fragment, preview modal) improve perceived load time; real content swaps in via HTMX fragment requests. +- Metrics endpoint `/themes/metrics` (gated by `WEB_THEME_PICKER_DIAGNOSTICS=1`) exposes: + - Catalog filter requests, cache hits, cache entry count + - Preview requests, cache hits, average build time (ms), cache size, TTL +- Response headers: `X-ThemeCatalog-Filter-Duration-ms` and `ETag` let you observe server-side filter cost and enable conditional requests. + +### Governance & Editorial +The theme catalog follows lightweight governance so curation quality scales: + +1. Minimum Examples Threshold: Target 2 example cards and 1 example commander for every established theme. Enforcement flips from warning to required once coverage >90% (tracked via metrics). Until then, themes below threshold are flagged but not blocked. +2. Curation Ordering: Preview assembly order is deterministic — examples, curated synergy examples, sampled role cards (payoff/enabler/support/wildcard), then synthetic placeholders only if needed to reach target count. +3. Splash Relax Policy: Four- and five-color commanders may include a single off-color enabler (scored with a -0.3 penalty) to avoid over-pruning multi-color creative lines. This is documented to prevent accidental removal during future sampling refactors. +4. Popularity Buckets Are Non-Scoring: Popularity is for filtering and subtle UI hints only and must never directly influence sampling scores (avoids positive feedback loops that crowd out niche archetypes). +5. Taxonomy Expansion Criteria: Proposed new high-level themes (Combo, Storm, Extra Turns, Group Hug / Politics, Pillowfort, Toolbox / Tutors, Treasure Matters, Monarch / Initiative) must demonstrate: (a) a distinct strategic pattern, (b) at least 8 broadly played representative cards, (c) not a strict subset of another existing theme. +6. Editorial Quality Tracking: Optional `editorial_quality: draft|reviewed|final` in theme YAML guides review pass prioritization; metrics aggregate coverage to spot stagnation. +7. Deterministic Sampling: Seed = `theme|commander` hash; changes to sampling heuristics must preserve deterministic output for identical inputs (regression tests rely on this). Score contributors must append reasons for transparency (`reasons[]`). + +For deeper rationale & checklist see `docs/theme_taxonomy_rationale.md`. +- Preview sampling caches results (TTL 600s) keyed by (slug, limit, colors, commander, ETag) ensuring catalog changes invalidate prior samples. + +Governance tooling & experiments: +- Taxonomy snapshot: `python -m code.scripts.snapshot_taxonomy` writes `logs/taxonomy_snapshots/taxonomy_.json` with a SHA-256 `hash` for auditability. Identical content is skipped unless forced. Use this before tuning taxonomy-aware sampling heuristics. +- Adaptive splash penalty (optional): set `SPLASH_ADAPTIVE=1` to scale off-color enabler penalties based on commander color count; tune with `SPLASH_ADAPTIVE_SCALE` (e.g., `1:1.0,2:1.0,3:1.0,4:0.6,5:0.35`). Analytics recognize both static and adaptive reasons; experiments can compare counters when enabled vs disabled. + +Preview endpoint: +`GET /themes/api/theme/{id}/preview?limit=12&colors=G,W&commander=Name` +Returns a deterministic sample (seeded by theme + optional commander) combining: +- Pinned curated `example_cards` (role `example`) +- Pinned curated `synergy_example_cards` (role `curated_synergy`) +- Heuristically sampled cards bucketed into payoff / enabler / support / wildcard with diversity quotas (~40/40/20 after pinned examples) +- Synthetic `[Synergy]` placeholders only if real sampling underfills the requested limit +Commander bias heuristics (color identity restriction + overlap/theme bonuses) influence scoring & ordering. + +Preview sampling now includes curated examples, curated synergy layer, role-based heuristic sampling (payoff / enabler / support / wildcard) with diversity quotas, commander bias heuristics (color identity restriction + diminishing overlap bonuses), rarity diminishing weights, splash leniency (single off-color allowance with small penalty for 4–5 color commanders), role saturation penalty, refined overlap scaling curve, and tooltip-expanded heuristic reasons. Synthetic placeholders are only added if the sample underfills. + +Edge considerations (Theme picker & preview): +- Empty dataset: graceful skeleton + 'No themes match your filters' message. +- High latency first load: skeleton shimmer + optional prewarm (when `WEB_THEME_FILTER_PREWARM=1`). +- Catalog reload or tagging completion: filter + preview caches bust; stale indicator triggers background refresh. +- Commander not found: commander bias reasons omitted; sampling still deterministic via seed. +- Color filter reduces pool below quota: synthetic `[Synergy]` placeholders pad to requested limit. +- Duplicate card names across tag shards: final defensive dedupe pass before payload. +- Oversized preview cache: FIFO eviction maintains <= THEME_PREVIEW_CACHE_MAX (default 400). +- Disabled diagnostics: uncapped synergies, editorial quality, fallback description markers, raw YAML & metrics all suppressed. +- Tooltip safety: JS errors are swallowed; preview remains functional without enhancements. + +#### Editorial coverage metrics / specialization progress +- Set `EDITORIAL_INCLUDE_FALLBACK_SUMMARY=1` when running `build_theme_catalog.py` to embed a `description_fallback_summary` block in the resulting JSON. This includes: + - `generic_total`, `generic_with_synergies`, `generic_plain`, and `generic_pct` + - `top_generic_by_frequency` (capped list of the highest-frequency themes still using generic fallback phrasing) +- Used by `test_theme_description_fallback_regression.py` to enforce a shrinking ceiling on generic descriptions after each optimization wave. + - History persisted to `description_fallback_history.jsonl` for KPI trend visualization and automated threshold ratcheting. + +#### External description mapping +- Extend or override auto-description heuristics without code edits via `config/themes/description_mapping.yml`. + - Each list item: `triggers: ["substring1", ...]`, `description: "Text with optional {SYNERGIES} placeholder"` + - First match wins; triggers are lowercase substring checks. + - `{SYNERGIES}` is replaced with a short clause (e.g., `Synergies like Tokens and Sacrifice reinforce the plan.`) when synergy context exists; omitted cleanly otherwise. +- If the file is absent, the internal mapping rules are used as a fallback. + +### Quick guide (locks • replace • compare • permalinks) +- New Deck modal (Steps 1–3 unified): search for your commander, pick themes, choose bracket (defaults to 3), and optionally set Name. Submitting starts the build immediately. Name becomes the export filename stem and display name. +- Lock a card: click the card image or the lock control in Step 5. Locked cards are pinned for reruns. A small “Last action” chip confirms lock/unlock. +- Replace a card: toggle Replace in Step 5, then click a card to open Alternatives. Filter (including Owned-only) and pick a swap; the deck updates in place. + - When bracket violations exist, Step 5 shows them first and disables Continue/Rerun. Pick replacements inline; alternatives exclude commanders, locked, in-deck, and just-added cards and prefer role-consistent options. +- Permalink: use “Copy permalink” from Step 5 or a Finished deck to share/restore a run (commander, themes, bracket, ideals, flags). “Open Permalink…” lets you paste one to restore to review. +- Compare: from Finished Decks, open Compare and pick A and B (quick actions: Latest two, Swap A/B). Use Changed-only to focus diffs. Copy summary or Download .txt. +- Keyboard: In the New Deck modal, Enter selects the first commander result. Browser autofill is disabled for cleaner inputs. In Step 5, use L to lock/unlock the focused card, R to open Alternatives, and C to copy a permalink. + +### Setup speed: parallel tagging (Web) +- On first run or when data is stale, the Web UI prepares and tags the card database. +- This tagging runs in parallel by default for faster setup. +- Tuning via env vars (set on the `web` service in `docker-compose.yml`): + - `WEB_TAG_PARALLEL=1|0` — enable/disable parallel workers (default: 1/enabled) + - `WEB_TAG_WORKERS=` — number of worker processes (default: 4 in compose; omit to auto-pick) +- The UI shows progress and falls back to sequential tagging automatically if parallel init fails. + +Virtualized lists and image polish (opt-in) +- Opt-in with `WEB_VIRTUALIZE=1` to enable virtualization in Step 5 lists/grids and the Owned library for smoother scrolling on large sets. +- Virtualization diagnostics overlay (for debugging): enable `SHOW_DIAGNOSTICS=1`, then press `v` to toggle an overlay per grid and a global summary bubble; shows visible range, total, rows, row height, render time, and counters. +- Thumbnails use lazy-loading with srcset/sizes; LQIP blur/fade-in is applied to Step 5 and Owned thumbnails, and the commander preview image. +- Respects reduced motion: when the OS prefers-reduced-motion, blur/fade transitions and smooth scrolling are disabled for accessibility. + +### Build options (Web) +- Use only owned cards: builds strictly from your `owned_cards/` lists (commander exempt). +- Prefer owned cards: gently prioritizes owned cards across creatures and spells but allows unowned when they’re better fits. + - Implemented via a stable owned-first reorder and small weight boosts; preserves existing sort intent. +Notes: +- Steps 1–3 are consolidated into a single New Deck modal. Submitting starts the build immediately, skipping the old review page. + - The modal includes an optional Name; when set, it is used as the export filename stem and display name in the UI. + - The modal displays selected themes in order (1, 2, 3). The Bracket selector shows numbers (e.g., "Bracket 3: Upgraded") and defaults to 3 for new decks. + +### Staged build visibility +- Step 5 can optionally “Show skipped stages” so you can see phases that added no cards with a clear annotation. + +### Multi-copy archetypes (Web) +- When your commander + themes suggest a multi-copy strategy (e.g., petitioners, approach, apostles), the Web UI shows a one-time modal to add a package. +- Choose how many copies (bounded by the printed cap) and optionally include 1× Thrumming Stone when it synergizes. +- The package is applied as the first stage so later phases account for the volume. +- Targets adjust automatically (reduce creatures for creature packages, or spread reductions across spells). The UI shows an “Adjusted targets” note on that stage. +- A final safety clamp trims overflow from this stage to keep the deck at 100. A “Clamped N” chip appears when this happens. +- Suggestions won’t re-prompt repeatedly for the same commander+theme context unless you change selections; you can also dismiss the modal. + +### Visual summaries (Web) +- Mana Curve bars with hover-to-highlight of matching cards in both list and thumbnail views +- Color Pips (requirements) and Sources (generation) bars with per-color tooltips listing cards +- Cross-highlighting from charts to the card views; list-mode highlights only the name span +- Sources detection includes non-land producers (artifacts/creatures/etc.) and colorless 'C' +- Fetch lands are not counted as mana sources; basic lands and Wastes are handled reliably +- Optional: Show colorless (C) toggle for Sources; persisted per browser +- Tooltips include a Copy button to copy the card list + +### Combos & Synergies (Web) +- Detection: curated two-card combos/synergies are detected from the final deck (commander included) and shown with list version badges. +- UI: chip-style rows with badges (cheap/early, setup). Hover a row to preview both cards side-by-side; hover an individual name for single-card preview. +- Auto-Complete Combos: when enabled in the New Deck modal, the builder completes up to N pairs before theme fill/monolithic spells so partners stick. + - Settings: Prefer combos (on/off), How many combos (target), Balance (early/late/mix) to bias partner selection. + - Existing completed pairs are counted first; only missing partners are added. + - Color identity enforced via the filtered card pool; off-color/unavailable partners are skipped. + +### Owned page (Web) +- View and manage your owned lists with: + - Export TXT/CSV, sort controls, and a live “N shown” counter + - Color identity dots and exact color-identity combo filters (including 4-color) + - Viewport-filling list with styled scrollbar + - Optional virtualization when `WEB_VIRTUALIZE=1` (improves performance on very large libraries) +- Uploading `.txt` or `.csv` lists enriches and deduplicates entries at upload-time and persists them, so page loads are fast. + +### Finished Decks (Web) +- Theme filters are a dropdown with shareable state for easier browsing of saved builds. + - Each finished deck has a permalink you can copy and revisit; Compare mode lets you diff against another run. The Compare page has a Copy summary button to copy a plain-text diff. + - Locks and Replace flow: lock-in picks you like or replace a card with an alternative during iteration. + +### Diagnostics (Web) +- Health endpoint: `GET /healthz` returns `{ status, version, uptime_seconds }` +- Responses include `X-Request-ID` for easier error correlation; unhandled errors return JSON with `request_id` + +Logs and system tools: +- Logs page (`/logs`, enable with `SHOW_LOGS=1`): + - Auto-refresh toggle with adjustable interval + - Level filter (All/Error/Warning/Info/Debug) and keyword filter + - Copy button to copy the visible log tail +- System summary (`GET /status/sys`, shown on `/diagnostics` when `SHOW_DIAGNOSTICS=1`): + - Returns `{ version, uptime_seconds, server_time_utc, flags }` + - Shows resolved theme and stored preference; includes a Reset preference button. + +Compose: enable diagnostics/logs (optional) + +```yaml +services: + web: + environment: + - SHOW_LOGS=1 + - SHOW_DIAGNOSTICS=1 + # Random Modes (optional; alpha) + - RANDOM_MODES=1 + - RANDOM_UI=1 + - RANDOM_MAX_ATTEMPTS=5 + - RANDOM_TIMEOUT_MS=5000 + # RANDOM_BUILD_SUPPRESS_INITIAL_EXPORT: # (Default behavior auto-enables suppression; set to 0 to debug legacy double-export) +``` + +### Budget Mode and price/legal status +- Price and legality snippet integration is deferred to a dedicated Budget Mode initiative. This will centralize price sourcing, caching, legality checks, and UI surfaces. Track progress in `logs/roadmaps/roadmap_9_budget_mode.md`. + +## 🤖 Headless mode (no prompts) + +- Auto-headless: set `DECK_MODE=headless` + - Example (PowerShell): + ```powershell + docker compose run --rm -e DECK_MODE=headless mtg-deckbuilder + ``` +- Use a JSON config: mount `./config` to `/app/config` and set `DECK_CONFIG=/app/config/deck.json` + - Example (PowerShell): + ```powershell + docker compose run --rm ` + -e DECK_MODE=headless ` + -e DECK_CONFIG=/app/config/deck.json ` + mtg-deckbuilder + ``` +- Override via env vars (subset): `DECK_COMMANDER`, `DECK_PRIMARY_CHOICE`, `DECK_SECONDARY_CHOICE`, `DECK_TERTIARY_CHOICE`, `DECK_ADD_LANDS`, `DECK_FETCH_COUNT`, `DECK_BRACKET_LEVEL` + - Precedence: CLI > env > JSON > defaults + +### Enhanced CLI with Type Safety & Theme Names + +The headless runner now features comprehensive CLI arguments with type indicators and intelligent theme selection: + +```powershell +# Show all available options with type information +python code/headless_runner.py --help + +# Build with theme names instead of index numbers +python code/headless_runner.py ` + --commander "Aang, Airbending Master" ` + --primary-tag "Airbending" ` + --secondary-tag "Exile Matters" ` + --bracket-level 4 + +# Override ideal deck composition counts +``` + +## 🧪 Theme Catalog Validation & Schema + +The theme system now uses a merged catalog plus per-theme YAML files. + +Validator usage: + +```powershell +python code/scripts/validate_theme_catalog.py # Standard validation +python code/scripts/validate_theme_catalog.py --rebuild-pass # Ensures idempotent rebuild +python code/scripts/validate_theme_catalog.py --schema # Catalog JSON Schema +python code/scripts/validate_theme_catalog.py --yaml-schema # Per-file YAML JSON Schema +python code/scripts/validate_theme_catalog.py --strict-alias # Fail if alias display_name still present +``` + +CI runs the non-strict validator every push. A second step runs `--strict-alias` as an allowed failure until legacy alias YAMLs (e.g., `reanimator.yml`) are removed, after which the step will be enforced. + +Per-file YAML structure (excerpt): + +```yaml +id: plus1-plus1-counters +display_name: +1/+1 Counters +curated_synergies: [Proliferate, Counters Matter] +enforced_synergies: [] +inferred_synergies: [] +synergies: [Proliferate, Counters Matter, Adapt, Evolve] +primary_color: Green +secondary_color: White +notes: '' +``` + +Add or edit a theme by creating/updating a file in `config/themes/catalog/` and running the build script: + +```powershell +python code/scripts/build_theme_catalog.py +``` + +Metadata info (formerly provenance) is written to `theme_list.json` and consumed by tests and future UI diagnostics. During the deprecation period the builder accepts either key; prefer `metadata_info` in all new tooling / docs. + +### Duplicate suppression & example_cards quality controls + +Example card generation (and rebuild / padding via `synergy_promote_fill.py`) includes safeguards to reduce noisy staples and over-represented picks: + +- `--common-card-threshold `: Excludes candidate example cards that already appear in more than this fraction of themes (default 0.18 = 18%). This curbs ubiquitous staples (e.g., Sol Ring) from crowding lists. +- `--print-dup-metrics`: After a run (dry-run or apply) prints global frequency distribution so thresholds can be tuned. + +Typical loop (dry run with metrics): + +```powershell +python code/scripts/synergy_promote_fill.py --fill-example-cards --common-card-threshold 0.18 --print-dup-metrics +``` + +Adjustment guidance: +- If many mid-frequency utility cards still leak through: lower the threshold (e.g., 0.15). +- If high-value, legitimately thematic cards are being suppressed prematurely: raise slightly (e.g., 0.22) and re-run with metrics to confirm net effect. + +These controls complement existing staple suppression heuristics (`staples_block` list) and color-fallback restraint flags. + +## 📝 Theme Editorial Enrichment + +Phase D introduces automated editorial helpers to surface representative cards and synergy-informed commander suggestions per theme while preserving manual curation: + +### Key scripts +- `code/scripts/generate_theme_editorial_suggestions.py`: Scans card & commander CSVs to propose `example_cards`, `example_commanders`, and a derived `synergy_commanders` list. + - Synergy commanders: up to 3 / 2 / 1 picks from the theme's top three synergies (3/2/1 pattern), falling back to legendary card hits if a synergy lacks direct commander-tagged entries. + - Promotion: ensures a minimum (default 5) example commanders by promoting synergy picks with annotations. + - Annotation format: `Name - Synergy (Synergy Theme)` for both promoted examples and residual synergy-only entries. + - Duplicate filtering: commanders already present (base name) in `example_commanders` are omitted from `synergy_commanders` to avoid redundancy. + - Augmentation (`--augment-synergies`): heuristically pads sparse `synergies` arrays (e.g., injects `Counters Matter`, `Proliferate` for counter variants) before deriving synergy picks. +- `code/scripts/lint_theme_editorial.py`: Validates editorial consistency (annotation correctness, min counts, deduplication, size bounds) and emits warnings (non-fatal by default). + +### Usage examples +```powershell +# Dry run (first 25 themes) +python code/scripts/generate_theme_editorial_suggestions.py + +# Apply to all themes with augmentation and minimum example commanders of 5 +python code/scripts/generate_theme_editorial_suggestions.py --apply --augment-synergies --min-examples 5 + +# Lint results +python code/scripts/lint_theme_editorial.py +``` + +### Non-determinism & source control guidance +The editorial output depends on current CSV card data, ranks, and tagging heuristics. Regeneration on a different machine or after data refresh may yield a different ordering or composition. For that reason: +- Commit the tooling and schema changes. +- Prefer selective or manually reviewed YAML edits (especially cornerstone themes) instead of bulk committing the entire regenerated catalog. +- Treat full-catalog regeneration as an operational task (e.g., during a release preparation) rather than every feature branch. + +### Schema fields +Per-theme YAML now may include: +```yaml +example_cards: [ ... ] # Representative non-commander cards +example_commanders: [ ... ] # Curated + promoted synergy commanders (annotated) +synergy_commanders: [ ... ] # Remaining annotated synergy picks not already promoted +``` + +### Lint expectations (default) +- example_commanders: <=12, aim for ≥5 (warnings below minimum) +- example_cards: ≤20 +- synergy_commanders: ≤6 (3/2/1 pattern) after filtering duplicates +- Annotations must reference a synergy present in `synergies`. + +Cornerstone themes (Landfall, Reanimate, Superfriends, Tokens Matter, +1/+1 Counters) should always have curated coverage; automated promotion fills gaps but manual review is encouraged. +python code/headless_runner.py ` + --commander "Krenko, Mob Boss" ` + --primary-tag "Goblin Kindred" ` + --creature-count 35 ` + --land-count 33 ` + --ramp-count 12 + +# Include/exclude specific cards (semicolon for comma-containing names) +python code/headless_runner.py ` + --commander "Jace, Vryn's Prodigy" ` + --include-cards "Sol Ring;Jace, the Mind Sculptor" ` + --exclude-cards "Chaos Orb;Shahrazad" ` + --enforcement-mode strict +``` + +**New CLI Features:** +- **Type-safe help text**: All arguments show expected types (PATH, NAME, INT, BOOL) +- **Ideal count arguments**: `--ramp-count`, `--land-count`, `--creature-count`, etc. +- **Theme tag names**: `--primary-tag "Theme Name"` instead of `--primary-choice 1` +- **Include/exclude CLI**: `--include-cards`, `--exclude-cards` with semicolon support +- **Console diagnostics**: Detailed summary output for validation and results + +Headless submenu notes: +- If one JSON exists in `config/`, it auto-runs it +- If multiple exist, they’re listed as "Commander - Theme1, Theme2, Theme3"; `deck.json` shows as "Default" +- CSV/TXT are exported as usual; JSON run-config is exported only in interactive runs + +### Run locally (no Docker) +```powershell +# Show resolved settings (no run) +python code/headless_runner.py --config config/deck.json --dry-run + +# Run with a specific config file +python code/headless_runner.py --config config/deck.json + +# Point to a folder; if exactly one config exists, it's auto-used +python code/headless_runner.py --config config + +# Override via CLI +python code/headless_runner.py --commander "Pantlaza, Sun-Favored" --primary-choice 2 --secondary-choice 0 --add-lands true --fetch-count 3 +``` + +### CLI flag reference + +Each flag below mirrors an environment variable (env vars override JSON, CLI overrides both). Types match `--help` output. + +#### Core selection & flow + +| Flag | Type | Description | JSON key / Env var | +| --- | --- | --- | --- | +| `--config PATH` | string | Path to JSON config file or directory (auto-detects a single file) | `DECK_CONFIG` | +| `--commander NAME` | string | Commander search term | `commander` / `DECK_COMMANDER` | +| `--primary-choice INT` | int | Primary theme menu index (1-based) | `primary_choice` / `DECK_PRIMARY_CHOICE` | +| `--secondary-choice INT` | optional int | Secondary theme index | `secondary_choice` / `DECK_SECONDARY_CHOICE` | +| `--tertiary-choice INT` | optional int | Tertiary theme index | `tertiary_choice` / `DECK_TERTIARY_CHOICE` | +| `--primary-tag NAME` | string | Primary theme name (auto-maps to index) | `primary_tag` / `DECK_PRIMARY_TAG` | +| `--secondary-tag NAME` | string | Secondary theme name | `secondary_tag` / `DECK_SECONDARY_TAG` | +| `--tertiary-tag NAME` | string | Tertiary theme name | `tertiary_tag` / `DECK_TERTIARY_TAG` | +| `--bracket-level 1-5` | int | Power bracket selection | `bracket_level` / `DECK_BRACKET_LEVEL` | +| `--dry-run` | flag | Print the resolved configuration and exit | — | + +#### Deck composition & counts + +| Flag | Type | Description | JSON key / Env var | +| --- | --- | --- | --- | +| `--ramp-count INT` | int | Target ramp spells | `ideal_counts.ramp` | +| `--land-count INT` | int | Total land target | `ideal_counts.lands` | +| `--basic-land-count INT` | int | Minimum basic lands | `ideal_counts.basic_lands` | +| `--creature-count INT` | int | Creature count target | `ideal_counts.creatures` | +| `--removal-count INT` | int | Spot removal target | `ideal_counts.removal` | +| `--wipe-count INT` | int | Board wipe target | `ideal_counts.wipes` | +| `--card-advantage-count INT` | int | Card advantage target | `ideal_counts.card_advantage` | +| `--protection-count INT` | int | Protection spell target | `ideal_counts.protection` | +| `--fetch-count INT` | optional int | Requested fetch land count | `fetch_count` / `DECK_FETCH_COUNT` | +| `--dual-count INT` | optional int | Dual land request | `dual_count` / `DECK_DUAL_COUNT` | +| `--triple-count INT` | optional int | Three-color land request | `triple_count` / `DECK_TRIPLE_COUNT` | +| `--utility-count INT` | optional int | Utility land request | `utility_count` / `DECK_UTILITY_COUNT` | + +#### Card type toggles + +| Flag | Type | Description | JSON key / Env var | +| --- | --- | --- | --- | +| `--add-lands BOOL` | bool | Run the land builder sequence | `add_lands` / `DECK_ADD_LANDS` | +| `--add-creatures BOOL` | bool | Add creatures | `add_creatures` / `DECK_ADD_CREATURES` | +| `--add-non-creature-spells BOOL` | bool | Bulk add non-creature spells (if available) | `add_non_creature_spells` / `DECK_ADD_NON_CREATURE_SPELLS` | +| `--add-ramp BOOL` | bool | Add ramp spells | `add_ramp` / `DECK_ADD_RAMP` | +| `--add-removal BOOL` | bool | Add removal spells | `add_removal` / `DECK_ADD_REMOVAL` | +| `--add-wipes BOOL` | bool | Add board wipes | `add_wipes` / `DECK_ADD_WIPES` | +| `--add-card-advantage BOOL` | bool | Add card draw | `add_card_advantage` / `DECK_ADD_CARD_ADVANTAGE` | +| `--add-protection BOOL` | bool | Add the protection suite | `add_protection` / `DECK_ADD_PROTECTION` | + +#### Include / exclude controls + +| Flag | Type | Description | JSON key / Env var | +| --- | --- | --- | --- | +| `--include-cards LIST` | string | Force-include cards (comma or semicolon separated) | `include_cards` | +| `--exclude-cards LIST` | string | Exclude cards | `exclude_cards` | +| `--enforcement-mode warn|strict` | string | Handling when includes cannot be satisfied | `enforcement_mode` | +| `--allow-illegal BOOL` | bool | Permit non-legal cards in include/exclude lists | `allow_illegal` | +| `--fuzzy-matching BOOL` | bool | Enable fuzzy card name matching | `fuzzy_matching` | + +#### Random mode (web parity) + +| Flag | Type | Description | Env var / JSON key | +| --- | --- | --- | --- | +| `--random-mode` | flag | Force the headless random builder path | `HEADLESS_RANDOM_MODE` / `random_mode` | +| `--random-theme NAME` | string | Legacy single-theme alias (maps to primary) | `RANDOM_THEME` / `random.theme` | +| `--random-primary-theme NAME` | string | Primary theme slug | `RANDOM_PRIMARY_THEME` / `random.primary_theme` | +| `--random-secondary-theme NAME` | string | Secondary theme slug | `RANDOM_SECONDARY_THEME` / `random.secondary_theme` | +| `--random-tertiary-theme NAME` | string | Tertiary theme slug | `RANDOM_TERTIARY_THEME` / `random.tertiary_theme` | +| `--random-auto-fill BOOL` | bool | Auto-fill missing theme slots | `RANDOM_AUTO_FILL` / `random.auto_fill` | +| `--random-auto-fill-secondary BOOL` | bool | Override secondary auto-fill | `RANDOM_AUTO_FILL_SECONDARY` / `random.auto_fill_secondary` | +| `--random-auto-fill-tertiary BOOL` | bool | Override tertiary auto-fill | `RANDOM_AUTO_FILL_TERTIARY` / `random.auto_fill_tertiary` | +| `--random-strict-theme-match BOOL` | bool | Require exact theme matches when selecting commanders | `RANDOM_STRICT_THEME_MATCH` / `random.strict_theme_match` | +| `--random-attempts INT` | int | Retry attempts before giving up | `RANDOM_MAX_ATTEMPTS` / `random.attempts` | +| `--random-timeout-ms INT` | int | Timeout per attempt (milliseconds) | `RANDOM_TIMEOUT_MS` / `random.timeout_ms` | +| `--random-seed VALUE` | int or string | Deterministic seed for reproducible runs | `RANDOM_SEED` / `random.seed` | +| `--random-constraints JSON_OR_PATH` | string | Inline JSON or path to constraint file | `RANDOM_CONSTRAINTS` / `RANDOM_CONSTRAINTS_PATH` / `random.constraints` | +| `--random-output-json PATH` | string | Write random build payload to file or directory | `RANDOM_OUTPUT_JSON` / `random.output_json` | + +Booleans accept: 1/0, true/false, yes/no, on/off. + +### Random mode parity (web → headless) + +The headless runner now shares the web UI's random builder pipeline. Set `--random-mode` (or `HEADLESS_RANDOM_MODE=1`) to route through the Surprise/Reroll flow with multi-theme support, auto-fill assistance, constraints, and deterministic seeds. Combine flags as needed: + +```powershell +python code/headless_runner.py ` + --random-mode ` + --random-primary-theme Artifacts ` + --random-secondary-theme Tokens ` + --random-auto-fill true ` + --random-output-json deck_files/random/latest.json +``` + +The build prints a full summary (commander, seed, theme stack, fallback reasons, attempts vs timeout, auto-fill status) and writes the optional JSON payload when `--random-output-json`/`RANDOM_OUTPUT_JSON` is provided. Use `--random-constraints` (or `RANDOM_CONSTRAINTS[_PATH]`) to supply pool limits, and `--random-seed` for reproducible reruns. Pair any random flag with `--dry-run` to inspect the resolved configuration without building. + +### JSON export in headless +- By default, headless runs do not export a JSON run-config to avoid duplicates. +- Opt-in with: + ```powershell + $env:HEADLESS_EXPORT_JSON = "1" + python code/headless_runner.py --config config/deck.json + ``` +- Tip: when opting in, prefer using `--config` instead of a `DECK_CONFIG` file path to avoid creating both a stem-based JSON and a second explicit-path JSON. + +Example JSON (`config/deck.json`): +```json +{ + "commander": "Pantlaza", + "bracket_level": 3, + "primary_choice": 2, + "secondary_choice": 2, + "tertiary_choice": 2, + "tag_mode": "OR", // OR or AND; Web UI default is OR + "add_lands": true, + "fetch_count": 3, + "ideal_counts": { "ramp": 10, "lands": 36, "basic_lands": 16, "creatures": 28, "removal": 8, "wipes": 3, "card_advantage": 8, "protection": 3 } +} +``` +Notes: headless honors `ideal_counts` (leave prompts blank to accept). Only `fetch_count` is tracked/exported for lands. + +#### JSON fields for Combos (Web Configs) +When running from a JSON config in the Web UI, the following fields are supported and exported from interactive runs: +```json +{ + "prefer_combos": true, + "combo_target_count": 3, + "combo_balance": "mix" +} +``` +Auto-Complete Combos runs before theme fill/monolithic spells when `prefer_combos` is true. + +## 🔒 Bracket policy & enforcement + +- Policy source: `config/brackets.yml` defines per-bracket limits for categories like `game_changers`, `extra_turns`, `mass_land_denial`, `tutors_nonland`, and `two_card_combos`. +- Authoritative lists: JSON under `config/card_lists/` provides names for enforcement and reporting (`game_changers.json`, `extra_turns.json`, `mass_land_denial.json`, `tutors_nonland.json`). A `list_version` may be included for badges. +- Global safety prune: when a category has a limit of 0, the builder removes matching cards from the card pool up-front so they cannot be selected (Game Changers by name; others by tags when present). This runs in both Web and headless builds. +- Preemptive filters: spells and creatures phases also apply bracket-aware pre-filters. +- Inline enforcement (Web): if violations still occur, Step 5 shows them before the summary. You must replace or remove flagged cards before you can Continue or Rerun. Alternatives are role-consistent, exclude the replaced/in-deck/locked/commander cards, and bias toward owned when enabled. +- Game Changer fallback: if `config/card_lists/game_changers.json` is empty, enforcement and reporting fall back to the in-code `builder_constants.GAME_CHANGERS` list so low-bracket decks still exclude those cards. +- Auto-enforce (optional): set `WEB_AUTO_ENFORCE=1` to automatically apply the enforcement plan after build and re-export CSV/TXT when violations are detected. + +Status levels +- PASS: within limits; panel remains collapsed by default. +- WARN: advisory thresholds met (from `_warn` keys in `config/brackets.yml` or conservative defaults for low brackets on tutors/extra turns). Panel opens automatically and shows WARN tiles with a subtle amber border and badge; no gating or automatic enforcement. +- FAIL: over hard limits or disallowed categories for the selected bracket. Panel opens automatically, tiles show with red accents, and Continue/Rerun are disabled until resolved; enforcement actions are available. + +Compliance report +- Every run writes `[stem]_compliance.json` next to your deck exports, including per-category counts/limits, status (PASS/FAIL), detected cheap/early two-card combos, and list versions. +- Headless JSON builds and Web builds use the same engine; summary and exports are consistent. + +## 🧩 Theme combine mode (AND/OR) + +- OR (default): Recommend cards that match any selected themes, prioritizing overlap. +- AND: Prioritize multi-theme intersections. For creatures, an AND pre-pass first picks "all selected themes" creatures up to a cap, then fills by weighted overlap. + +UI tips: +- Step 2 includes AND/OR radios with a tooltip explaining trade-offs. +- In staged build view, "Creatures: All-Theme" shows which selected themes each card hits. + +## 🕹️ Usage (interactive) +1) Start the app (Docker or from source) +2) Pick Build a New Deck +3) Search/confirm commander +4) Pick primary/secondary/tertiary themes (or stop at primary); choose AND/OR combine mode +5) Choose power bracket and review ideal counts +6) Deck builds; CSV/TXT export to `deck_files/` + +## ⚙️ Environment variables (common) +- DECK_MODE=headless +- DECK_CONFIG=/app/config/deck.json +- DECK_COMMANDER, DECK_PRIMARY_CHOICE, DECK_SECONDARY_CHOICE, DECK_TERTIARY_CHOICE +- DECK_ADD_LANDS, DECK_FETCH_COUNT +- DECK_ADD_CREATURES, DECK_ADD_NON_CREATURE_SPELLS, DECK_ADD_RAMP, DECK_ADD_REMOVAL, DECK_ADD_WIPES, DECK_ADD_CARD_ADVANTAGE, DECK_ADD_PROTECTION +- DECK_BRACKET_LEVEL +- **ALLOW_MUST_HAVES=true** (enables include/exclude card lists feature with enhanced UI validation) +- Random Modes (alpha): core toggles `RANDOM_MODES=1`, `RANDOM_UI=1`, `RANDOM_MAX_ATTEMPTS=5`, `RANDOM_TIMEOUT_MS=5000` (see detailed list below for theme/auto-fill/seed overrides) + - `RANDOM_BUILD_SUPPRESS_INITIAL_EXPORT=0` — (optional) disable the default suppression of builder auto-export (will produce legacy duplicate artifacts; for debugging only) + - Testing/data overrides: + - `CSV_FILES_DIR=csv_files/testdata` — point the app/tests at a frozen dataset snapshot for determinism + +Random Modes (alpha flags): +- `RANDOM_MODES=1` — enable random build endpoints and backend features +- `RANDOM_UI=1` — show Surprise/Reroll/Share controls in UI +- `RANDOM_MAX_ATTEMPTS=5` — cap retry attempts for random generation +- `RANDOM_TIMEOUT_MS=5000` — per-build timeout in milliseconds +- `RANDOM_THEME=`*slug* — legacy single-theme alias (maps to primary when others unset) +- `RANDOM_PRIMARY_THEME=`*slug* — primary theme override +- `RANDOM_SECONDARY_THEME=`*slug* — secondary theme override +- `RANDOM_TERTIARY_THEME=`*slug* — tertiary theme override +- `RANDOM_AUTO_FILL=0|1` — auto-fill missing secondary/tertiary slots when enabled +- `RANDOM_AUTO_FILL_SECONDARY=0|1` / `RANDOM_AUTO_FILL_TERTIARY=0|1` — explicit per-slot overrides +- `RANDOM_STRICT_THEME_MATCH=0|1` — require exact theme matches when rolling commanders +- `RANDOM_CONSTRAINTS=`*inline JSON* and/or `RANDOM_CONSTRAINTS_PATH=/app/config/random_constraints.json` +- `RANDOM_SEED=`*value* — deterministic run seed (int or string) +- `RANDOM_OUTPUT_JSON=/app/deck_files/random_build.json` — write random build payload to file/dir +- `HEADLESS_RANDOM_MODE=1` — force the headless runner to take the random pipeline +- `RANDOM_BUILD_SUPPRESS_INITIAL_EXPORT=0` — (optional) disable the default suppression of builder auto-export (useful for debugging legacy duplicate exports) + +Testing/determinism helpers: +- CSV_FILES_DIR=csv_files/testdata — override CSV base dir for tests or pinned snapshots + +Optional name-based tag overrides (mapped to indices for the chosen commander): +- DECK_PRIMARY_TAG, DECK_SECONDARY_TAG, DECK_TERTIARY_TAG + +Combine mode in headless: +- JSON: set `"tag_mode": "AND" | "OR"` +- Env var: `DECK_TAG_MODE=AND|OR` (if configured in your environment) + +Web UI performance tuning: +- WEB_TAG_PARALLEL=1|0 +- WEB_TAG_WORKERS= + - WEB_VIRTUALIZE=1 (enable list virtualization) + - SHOW_DIAGNOSTICS=1 (optional: diagnostics tools and virtualization overlay toggle using `v`) + - WEB_AUTO_ENFORCE=1 (optional; after building, auto-apply enforcement and re-export when the compliance report fails) + - SPLASH_ADAPTIVE=1 (optional: enable adaptive off-color penalty by commander color count) + - SPLASH_ADAPTIVE_SCALE="1:1.0,2:1.0,3:1.0,4:0.6,5:0.35" (optional: tuning for adaptive scaling) + +Paths and data overrides: +- `DECK_EXPORTS=/app/deck_files` — where finished deck exports are read by the Web UI +- `OWNED_CARDS_DIR=/app/owned_cards` — preferred directory for owned inventory uploads +- `CARD_LIBRARY_DIR=/app/owned_cards` — back‑compat alias for OWNED_CARDS_DIR +- `DECK_CONFIG=/app/config/deck.json` — default headless config path +- `CSV_FILES_DIR=/app/csv_files` — override CSV base dir (tests, snapshots, or alternate datasets) +- `CARD_INDEX_EXTRA_CSV=path/to/synthetic_cards.csv` — inject an extra CSV into the card index for experiments/tests + +Theme catalog scan & prewarm: +- `THEME_CATALOG_YAML_SCAN_INTERVAL_SEC=2.0` — poll for YAML changes (dev); omit in production +- `WEB_THEME_FILTER_PREWARM=1` — prewarm common filters for faster first renders + +Theme preview cache & Redis (optional): +- `THEME_PREVIEW_CACHE_MAX=400` — max previews cached in memory +- `WEB_THEME_PREVIEW_LOG=0` — 1=verbose cache logs +- `THEME_PREVIEW_ADAPTIVE=0` — 1=adaptive cache policy +- `THEME_PREVIEW_EVICT_COST_THRESHOLDS=5,15,40` — cost tiers for eviction policy +- `THEME_PREVIEW_BG_REFRESH=0` — 1=background refresh worker; `THEME_PREVIEW_BG_REFRESH_INTERVAL=120` seconds +- TTL tuning: `THEME_PREVIEW_TTL_BASE=300`, `THEME_PREVIEW_TTL_MIN=60`, `THEME_PREVIEW_TTL_MAX=900`, `THEME_PREVIEW_TTL_BANDS=0.2,0.5,0.8`, `THEME_PREVIEW_TTL_STEPS=2,4,2,3,1` +- Redis: `THEME_PREVIEW_REDIS_URL=redis://localhost:6379/0`, `THEME_PREVIEW_REDIS_DISABLE=0` (1=disable redis even if URL set) + +Rarity weighting and diversity (advanced): +- `RARITY_W_MYTHIC=1.2`, `RARITY_W_RARE=0.9`, `RARITY_W_UNCOMMON=0.65`, `RARITY_W_COMMON=0.4` — diminish or boost rarity selection +- `RARITY_DIVERSITY_TARGETS="mythic:0-1,rare:0-2,uncommon:0-4,common:0-6"` — soft targets per rarity band +- `RARITY_DIVERSITY_OVER_PENALTY=-0.5` — penalty when exceeding targets + +Misc utility land tuning (Step 7): +- MISC_LAND_DEBUG=1 Write debug CSVs for misc land step (candidates/post-filter). Otherwise suppressed unless SHOW_DIAGNOSTICS=1. +- MISC_LAND_EDHREC_KEEP_PERCENT_MIN=0.75 Lower bound of dynamic EDHREC keep range (0–1). When MIN & MAX are both set, a random % in [MIN,MAX] is rolled per build. +- MISC_LAND_EDHREC_KEEP_PERCENT_MAX=1.0 Upper bound of dynamic EDHREC keep range (0–1). +- MISC_LAND_EDHREC_KEEP_PERCENT=0.80 Legacy fixed keep % used only if MIN/MAX not both present. +- MISC_LAND_THEME_MATCH_BASE=1.4 Base multiplier when at least one selected theme tag matches a candidate land. +- MISC_LAND_THEME_MATCH_PER_EXTRA=0.15 Incremental multiplier per additional matching theme tag beyond the first. +- MISC_LAND_THEME_MATCH_CAP=2.0 Cap for total theme multiplier after stacking base + extras. + +Notes: +- Fetch lands are fully excluded from the misc step (handled earlier). +- Mono-color decks auto-filter broad rainbow/any-color lands except an explicit always-keep list. +- Land Alternatives endpoint: for land seeds, returns land-only suggestions; 12 random picks each request from a randomly sized window within the top 60–100 ranked candidates (per-card, uncached) for variety. + +## 📁 Folders +- `deck_files/` — CSV/TXT exports +- `csv_files/` — card data +- `logs/` — logs +- `config/` — JSON configs (optional) +- `owned_cards/` — your owned cards lists (`.txt`/`.csv`); used for owned-only builds and Owned flagging + +## 🧰 Troubleshooting +- Use `docker compose run --rm` (not `up`) for interactive sessions +- Ensure volumes are mounted so files persist (`deck_files`, `logs`, `csv_files`) +- For headless with config, mount `./config:/app/config` and set `DECK_CONFIG` +- Card data refresh: if `csv_files/cards.csv` is missing or older than 7 days, the app refreshes data and re-tags automatically. A `.tagging_complete.json` file in `csv_files/` indicates tagging completion. + - Web health: the header dot polls `/healthz` — green is OK, red is degraded. If red, check logs. + - Error details: HTMX errors show a toast with a “Copy details” button including X-Request-ID; include that when filing issues. + - Logs page: set `SHOW_LOGS=1` to enable `/logs` and `/status/logs?tail=200` (read‑only) for quick diagnostics. + - Diagnostics page: set `SHOW_DIAGNOSTICS=1` to enable the nav link and `/diagnostics` test tools. + +Data integrity notes: +- Banned cards: per-color/guild CSVs now consistently respect the Commander banned list using exact, case‑insensitive name matching across `name` and `faceName`. + +## 🧪 Development and tests + +Set up a virtual environment, install dependencies, and run the test suite. + +```powershell +# From repo root (PowerShell) +python -m venv .venv +& ".venv/Scripts/Activate.ps1" +pip install -r requirements.txt +pip install -r requirements-dev.txt + +# Run tests (pytest is configured to look under code/tests) +python -m pytest -q +``` + +Notes: +- Test discovery is set to `code/tests` in `pytest.ini`. +- `pytest.ini` disables the built-in debugging plugin to avoid a stdlib module name clash with the project folder `code/`. +- Feature flags for local diagnostics: set `SHOW_DIAGNOSTICS=1` and/or `SHOW_LOGS=1` to expose `/diagnostics` and `/logs`. + - Index testing helper: set `CARD_INDEX_EXTRA_CSV=path\to\synthetic_cards.csv` to inject a temporary CSV during tests (e.g., color identity edge cases) without altering production shards. + +### What’s new (quick summary) +- Faster browsing with optional virtualized grids/lists in Step 5 and Owned (`WEB_VIRTUALIZE=1`). +- Image polish: lazy-loading, responsive `srcset/sizes`, and LQIP blur/fade for Step 5, Owned, and commander preview. +- Diagnostics overlay (opt-in with `SHOW_DIAGNOSTICS=1`): press `v` to see visible range, totals, render time, and counters. +- Accessibility: respects reduced-motion (disables blur/fade and smooth scrolling). +- Small caching wins: short‑TTL fragment caching for summary partials and suggestions. + +## 📦 Releases +- Release notes are maintained in `RELEASE_NOTES_TEMPLATE.md`. Automated workflows read from this file to populate Docker Hub and GitHub releases. + +### Collecting diagnostics for an issue +- Note the Request-ID from the toast or error page. +- Copy logs from `/logs` (enable with `SHOW_LOGS=1`) or `/status/logs?tail=200`. +- Include your `/healthz` JSON and environment flags (SHOW_LOGS/SHOW_DIAGNOSTICS/SHOW_SETUP) when reporting. + +## Performance Baselines + +A warm preview performance baseline is committed at `logs/perf/theme_preview_warm_baseline.json`. + +Tooling: +- Benchmark: `python -m code.scripts.preview_perf_benchmark --all --passes 2 --extract-warm-baseline logs/perf/theme_preview_warm_baseline.json` +- Compare (manual): `python -m code.scripts.preview_perf_compare --baseline logs/perf/theme_preview_warm_baseline.json --candidate logs/perf/new_run.json --warm-only --p95-threshold 5` +- Regression helper: `python -m code.scripts.preview_perf_ci_check --baseline logs/perf/theme_preview_warm_baseline.json --p95-threshold 5` (run locally or in ad-hoc CI when desired) + +Policy: +- Treat warm-only p95 regressions beyond 5% as blockers even though enforcement is now manual. +- Update the baseline only after intentional performance improvements or systemic environment shifts. + +Workflow to refresh baseline: +1. Ensure no active regressions (run wrapper locally, expect pass). +2. Run a multi-pass benchmark with at least two passes over all themes. +3. Replace `theme_preview_warm_baseline.json` with new warm pass output; commit in same PR with rationale in CHANGELOG. + +Optional future enhancements: +- Reintroduce automated gating once preview perf stabilizes on shared runners. +- Separate p50 threshold (e.g., >7% regression) for early warning without failing build. diff --git a/docs/headless_cli_guide.md b/docs/headless_cli_guide.md new file mode 100644 index 0000000..a3a6641 --- /dev/null +++ b/docs/headless_cli_guide.md @@ -0,0 +1,91 @@ +# Headless & CLI Guide + +Leverage the shared deckbuilding engine from the command line, in headless mode, or within containers. + +## Table of contents +- [Entry points](#entry-points) +- [Switching modes in Docker](#switching-modes-in-docker) +- [Headless JSON configs](#headless-json-configs) +- [Environment overrides](#environment-overrides) +- [CLI argument reference](#cli-argument-reference) +- [Include/exclude lists from the CLI](#includeexclude-lists-from-the-cli) +- [Practical examples](#practical-examples) + +--- + +## Entry points +- Interactive menu: `python code/main.py` +- Headless runner: `python code/headless_runner.py --config config/deck.json` +- Both executables share the same builder core used by the Web UI. + +## Switching modes in Docker +Override the container entrypoint to run the CLI or headless flows inside Docker Compose or plain `docker run`. + +```powershell +# Compose example +docker compose run --rm -e APP_MODE=cli web + +# Compose with headless automation +docker compose run --rm ` + -e APP_MODE=cli ` + -e DECK_MODE=headless ` + -e DECK_CONFIG=/app/config/deck.json ` + web +``` + +Set `APP_MODE=cli` to switch from the Web UI to the textual interface. Add `DECK_MODE=headless` to skip prompts and immediately run the configured deck. + +## Headless JSON configs +- Drop JSON files into `config/` (e.g., `config/deck.json`). +- Headless mode auto-runs the lone JSON file; if multiple exist, the CLI lists them with summaries (commander + themes). +- Config fields cover commander, bracket, include/exclude lists, theme preferences, owned-mode toggles, and output naming. + +## Environment overrides +When running in containers or automation, environment variables can override JSON settings. Typical variables include: +- `DECK_COMMANDER` +- `DECK_PRIMARY_CHOICE`, `DECK_SECONDARY_CHOICE`, `DECK_TERTIARY_CHOICE` +- `DECK_BRACKET_LEVEL` +- `DECK_ADD_LANDS`, `DECK_LAND_COUNT`, `DECK_CREATURE_COUNT`, `DECK_RAMP_COUNT` + +Precedence order: **CLI flags > environment variables > JSON config > defaults**. + +## CLI argument reference +Run `python code/headless_runner.py --help` to see the current argument surface. Highlights: + +- Type indicators make expectations explicit (e.g., `PATH`, `NAME`, `INT`). +- Theme selection accepts human-readable names: `--primary-tag "Airbending"` instead of numeric indexes. +- Bracket selection via `--bracket-level`. +- Ideal counts such as `--land-count`, `--ramp-count`, `--creature-count`, and more. + +## Include/exclude lists from the CLI +You can specify comma- or semicolon-separated lists directly through the CLI: + +```powershell +python code/headless_runner.py ` + --commander "Jace, Vryn's Prodigy" ` + --include-cards "Sol Ring;Jace, the Mind Sculptor" ` + --exclude-cards "Chaos Orb;Shahrazad" ` + --enforcement-mode strict +``` + +Semicolons allow card names containing commas. Enforcement modes mirror the Web UI (`off`, `warn`, `strict`). + +## Practical examples +```powershell +# Build a Goblins list with tuned counts +python code/headless_runner.py ` + --commander "Krenko, Mob Boss" ` + --primary-tag "Goblin Kindred" ` + --creature-count 35 ` + --land-count 33 ` + --ramp-count 12 + +# Fire a headless run via Docker using an alternate config folder +docker compose run --rm ` + -e APP_MODE=cli ` + -e DECK_MODE=headless ` + -e DECK_CONFIG=/app/config/custom_decks ` + web +``` + +The CLI prints a detailed summary at the end of each run, including enforcement results, resolved themes, and export paths. All artifacts land in the same `deck_files/` folder used by the Web UI. diff --git a/docs/theme_catalog_advanced.md b/docs/theme_catalog_advanced.md new file mode 100644 index 0000000..2d74cee --- /dev/null +++ b/docs/theme_catalog_advanced.md @@ -0,0 +1,148 @@ +# Theme Catalog Advanced Guide + +Additional details for developers and power users working with the theme catalog, editorial tooling, and diagnostics. + +## Table of contents +- [HTMX API endpoints](#htmx-api-endpoints) +- [Caching, diagnostics, and metrics](#caching-diagnostics-and-metrics) +- [Governance principles](#governance-principles) +- [Operational tooling](#operational-tooling) + - [Refreshing catalogs](#refreshing-catalogs) + - [Snapshotting taxonomy](#snapshotting-taxonomy) + - [Adaptive splash penalty experiments](#adaptive-splash-penalty-experiments) +- [Editorial pipeline](#editorial-pipeline) + - [Script summary](#script-summary) + - [Example configuration](#example-configuration) + - [Duplicate suppression controls](#duplicate-suppression-controls) + - [Coverage metrics and KPIs](#coverage-metrics-and-kpis) + - [Description mapping overrides](#description-mapping-overrides) +- [Validation and schema tooling](#validation-and-schema-tooling) + +--- + +## HTMX API endpoints +The upcoming theme picker UI is powered by two FastAPI endpoints. + +### `GET /themes/api/themes` +Parameters: +- `q`: substring search across theme names and synergies. +- `archetype`: filter by `deck_archetype`. +- `bucket`: popularity bucket (Very Common, Common, Uncommon, Niche, Rare). +- `colors`: comma-separated color initials (e.g. `G,W`). +- `limit` / `offset`: pagination (limit defaults to 50, max 200). +- `diagnostics=1`: surfaces `has_fallback_description` and `editorial_quality` (requires `WEB_THEME_PICKER_DIAGNOSTICS=1`). + +The response includes `count`, the filtered `items`, and `next_offset` for subsequent requests. Diagnostic mode adds extra telemetry fields. + +### `GET /themes/api/theme/{id}` +Parameters: +- `uncapped=1`: (diagnostics) returns `uncapped_synergies`, combining curated, enforced, and inferred sets. +- `diagnostics=1`: exposes editorial metadata such as `editorial_quality` and `has_fallback_description`. + +The payload merges curated data with editorial artifacts (`example_cards`, `example_commanders`, etc.) and respects the same diagnostic feature flag. + +## Caching, diagnostics, and metrics +- Responses include an `ETag` header derived from catalog metadata so consumers can perform conditional GETs. +- `/themes/status` reports freshness and stale indicators; `/themes/refresh` (POST) triggers a background rebuild. +- When `WEB_THEME_PICKER_DIAGNOSTICS=1` is set, the app records: + - Filter cache hits/misses and duration (`X-ThemeCatalog-Filter-Duration-ms`). + - Preview cache metrics (`/themes/metrics` exposes counts, hit rates, TTL, and average build time). +- Skeleton loaders ship with the HTMX fragments to keep perceived latency low. + +## Governance principles +To keep the catalog healthy, the project follows a lightweight governance checklist: + +1. **Minimum examples** – target at least two example cards and one commander per established theme. +2. **Deterministic preview assembly** – curated examples first, then role-based samples (payoff/enabler/support/wildcard), then placeholders if needed. +3. **Splash relax policy** – four- and five-color commanders may include a single off-color enabler with a small penalty, preventing over-pruning. +4. **Popularity buckets are advisory** – they guide filters and UI hints but never directly influence scoring. +5. **Taxonomy expansion bar** – new high-level archetypes require a distinct pattern, at least eight representative cards, and no overlap with existing themes. +6. **Editorial quality tiers** – optional `editorial_quality: draft|reviewed|final` helps prioritize review passes. +7. **Deterministic sampling** – seeds derive from `theme|commander` hashes; scoring code should emit `reasons[]` to explain decisions and remain regression-test friendly. + +See `docs/theme_taxonomy_rationale.md` for the underlying rationale and roadmap. + +## Operational tooling + +### Refreshing catalogs +- Primary builder: `python code/scripts/build_theme_catalog.py` +- Options: + - `--limit N`: preview a subset without overwriting canonical outputs (unless `--allow-limit-write`). + - `--output path`: write to an alternate path; suppresses YAML backfill to avoid mutating tracked files. + - `--backfill-yaml` or `EDITORIAL_BACKFILL_YAML=1`: fill missing descriptions and popularity buckets in YAML files. + - `--force-backfill-yaml`: overwrite existing description/popularity fields. + - `EDITORIAL_SEED=`: force a deterministic ordering when heuristics use randomness. + - `EDITORIAL_AGGRESSIVE_FILL=1`: pad sparse themes with inferred synergies. + - `EDITORIAL_POP_BOUNDARIES="a,b,c,d"`: tune popularity thresholds. + - `EDITORIAL_POP_EXPORT=1`: emit `theme_popularity_metrics.json` summaries. + +### Snapshotting taxonomy +`python -m code.scripts.snapshot_taxonomy` writes `logs/taxonomy_snapshots/taxonomy_.json` with a SHA-256 hash. Identical content is skipped unless you supply `--force`. Use snapshots before experimenting with taxonomy-aware sampling. + +### Adaptive splash penalty experiments +Set `SPLASH_ADAPTIVE=1` to scale off-color enabler penalties based on commander color count. Tune with `SPLASH_ADAPTIVE_SCALE` (e.g. `1:1.0,2:1.0,3:1.0,4:0.6,5:0.35`). Analytics aggregate both static and adaptive reasons for comparison. + +## Editorial pipeline + +### Script summary +- `code/scripts/generate_theme_editorial_suggestions.py` + - Proposes `example_cards`, `example_commanders`, and `synergy_commanders` using card CSVs and tagging heuristics. + - `--augment-synergies` can pad sparse `synergies` arrays prior to suggestion. + - `--apply` writes results; dry runs print suggestions for review. +- `code/scripts/lint_theme_editorial.py` + - Validates annotation formats, min/max counts, and deduplication. Combine with environment toggles (`EDITORIAL_REQUIRE_DESCRIPTION`, `EDITORIAL_REQUIRE_POPULARITY`) for stricter gating. + +### Example configuration +```powershell +# Dry run on the first 25 themes +python code/scripts/generate_theme_editorial_suggestions.py + +# Apply across the catalog with augmentation and min example commanders set to 5 +python code/scripts/generate_theme_editorial_suggestions.py --apply --augment-synergies --min-examples 5 + +# Lint results +python code/scripts/lint_theme_editorial.py +``` + +Editorial output depends on current CSV data. Expect ordering or composition changes after upstream dataset refreshes—treat full-catalog regeneration as an operational task and review diffs carefully. + +### Duplicate suppression controls +`code/scripts/synergy_promote_fill.py` can rebalance example cards: + +```powershell +python code/scripts/synergy_promote_fill.py --fill-example-cards --common-card-threshold 0.18 --print-dup-metrics +``` + +- `--common-card-threshold`: filters cards appearing in more than the specified fraction of themes (default `0.18`). +- Use metrics output to tune thresholds so staple utility cards stay in check without removing legitimate thematic cards. + +### Coverage metrics and KPIs +- `EDITORIAL_INCLUDE_FALLBACK_SUMMARY=1` embeds a `description_fallback_summary` block in the generated catalog (`generic_total`, `generic_plain`, `generic_pct`, etc.). +- Regression tests use these metrics to ratchet down generic descriptions over time. +- Historical trends are appended to `config/themes/description_fallback_history.jsonl` for analysis. + +### Description mapping overrides +Customize automatic descriptions without editing code: + +- Add `config/themes/description_mapping.yml` with entries: + ```yaml + - triggers: ["sacrifice", "aristocrat"] + description: "Leans on sacrifice loops and {SYNERGIES}." + ``` +- The first matching trigger wins (case-insensitive substring search). +- `{SYNERGIES}` expands to a short clause listing the top synergies when available, and disappears gracefully if not. +- Internal defaults remain as fallbacks when the mapping file is absent. + +## Validation and schema tooling + +Run validators to maintain catalog quality: + +```powershell +python code/scripts/validate_theme_catalog.py +python code/scripts/validate_theme_catalog.py --rebuild-pass +python code/scripts/validate_theme_catalog.py --schema +python code/scripts/validate_theme_catalog.py --yaml-schema +python code/scripts/validate_theme_catalog.py --strict-alias +``` + +Per-theme YAML files (under `config/themes/catalog/`) are tracked in source control. Keys such as `metadata_info` replace the legacy `provenance`; the validator treats missing migrations as warnings until the deprecation completes. diff --git a/docs/web_ui_deep_dive.md b/docs/web_ui_deep_dive.md new file mode 100644 index 0000000..051b4f3 --- /dev/null +++ b/docs/web_ui_deep_dive.md @@ -0,0 +1,103 @@ +# Web UI Deep Dive + +A closer look at the rich interactions available in the MTG Python Deckbuilder Web UI. Use this guide after you are comfortable with the basic homepage flows described in the README. + +## Table of contents +- [Unified New Deck modal](#unified-new-deck-modal) +- [Stage 5 tools: lock, replace, compare, permalinks](#stage-5-tools-lock-replace-compare-permalinks) +- [Multi-copy archetype packages](#multi-copy-archetype-packages) +- [Bracket compliance and skipped stages](#bracket-compliance-and-skipped-stages) +- [Build options: owned-only and prefer-owned](#build-options-owned-only-and-prefer-owned) +- [Visual summaries](#visual-summaries) +- [Combos & synergies](#combos--synergies) +- [Owned library page](#owned-library-page) +- [Finished decks workspace](#finished-decks-workspace) +- [Keyboard shortcuts](#keyboard-shortcuts) +- [Virtualization, tagging, and performance](#virtualization-tagging-and-performance) +- [Diagnostics and logs](#diagnostics-and-logs) + +--- + +## Unified New Deck modal +The first three steps of deckbuilding live inside a single modal: + +1. **Search for a commander** – autocomplete prioritizes color identity matches; press Enter to grab the top result. +2. **Pick primary/secondary/tertiary themes** – the modal displays your selections in order so you can revisit them quickly. +3. **Choose a bracket** – labels such as “Bracket 3: Upgraded” clarify power bands. Bracket 3 is the default tier for new builds. + +Optional inputs: +- **Deck name** becomes the export filename stem and is reused in Finished Decks banners. +- **Combo auto-complete** and other preferences persist between runs. + +Once you submit, the modal closes and the build starts immediately—no extra confirmation screen. + +## Stage 5 tools: lock, replace, compare, permalinks +Stage 5 is the iterative workspace for tuning the deck: + +- **Lock** a card by clicking the padlock or the card artwork. Locked cards persist across rerolls and show a “Last action” chip for quick confirmation. +- **Replace** opens the Alternatives drawer. Filters include Owned-only, role alignment, and bracket compliance. The system skips commanders, locked cards, just-added cards, and anything already in the list. +- **Permalink** buttons appear in Stage 5 and Finished Decks. Share a build (commander, themes, bracket, ideals, flags) or restore one by pasting a permalink back into the app. +- **Compare** mode lives in Finished Decks. Pick two builds (quick actions select the latest pair) and triage changes via Changed-only, Copy summary, or download the diff as TXT. + +## Multi-copy archetype packages +When a commander + theme combination suggests a multi-copy strategy (e.g., Persistent Petitioners, Shadowborn Apostles), the UI offers an optional package: + +- Choose the desired quantity (bounded by printed limits) and optionally add **Thrumming Stone** when it synergizes. +- Packages are inserted before other stages so target counts adjust appropriately. +- A safety clamp trims overflow to keep the deck at 100 cards; the stage displays a “Clamped N” indicator if it triggers. +- You can dismiss the modal, and we won’t re-prompt unless your selections change. + +## Bracket compliance and skipped stages +- Bracket policy enforcement prunes disallowed categories before stage execution. Violations block reruns until you resolve them. +- Enforcement options: keep the panel collapsed when compliant, auto-open with a colored status chip (green/amber/red) when action is needed. +- Enable auto-enforcement by setting `WEB_AUTO_ENFORCE=1`. +- Toggle **Show skipped stages** to surface steps that added zero cards, making it easier to review the full pipeline. + +## Build options: owned-only and prefer-owned +The modal includes toggles for **Use only owned cards** and **Prefer owned cards**: + +- Owned-only builds pull strictly from the inventory in `owned_cards/` (commander exempt). +- Prefer-owned bumps owned cards slightly in the scoring pipeline but still allows unowned all-stars when necessary. +- Both modes respect the Owned Library filters and show Owned badges in the exported CSV (including the `Owned` column when you disable the mode). + +## Visual summaries +Stage 5 displays multiple data visualizations that cross-link to the card list: + +- **Mana curve** – hover a bar to highlight matching cards in list and thumbnail views. +- **Color requirements vs. sources** – pips show requirements; sources include non-land producers and an optional `C` (colorless) toggle. +- **Tooltips** – each tooltip lists contributing cards and offers a copy-to-clipboard action. +- Visual polish includes lazy-loaded thumbnails, blur-up transitions, and accessibility tweaks that respect `prefers-reduced-motion`. + +## Combos & synergies +The builder detects curated two-card combos and synergy pairs in the final deck: + +- Chips display badges such as “cheap” or “setup” with hover previews for each card and a split preview when hovering the entire row. +- Enable **Auto-complete combos** to add missing partners before theme filling. Configure target count, balance (early/late/mix), and preference weighting. +- Color identity restrictions keep the algorithm from suggesting off-color partners. + +## Owned library page +Open the Owned tile to manage uploaded inventories: + +- Upload `.txt` or `.csv` files with one card per line. The app enriches and deduplicates entries on ingestion. +- The page includes sortable columns, exact color-identity filters (including four-color combos), and an export button. +- Large collections benefit from virtualization when `WEB_VIRTUALIZE=1`. + +## Finished decks workspace +- Browse historical builds with filterable theme chips. +- Each deck offers Download TXT, Copy summary, Open permalink, and Compare actions. +- Locks, replace history, and compliance metadata are stored per deck and surface alongside the exports. + +## Keyboard shortcuts +- **Enter** selects the first commander suggestion while searching. +- Inside Stage 5 lists: **L** locks/unlocks the focused card, **R** opens the Replace drawer, and **C** copies the permalink. +- Browser autofill is disabled in the modal to keep searches clean. + +## Virtualization, tagging, and performance +- `WEB_TAG_PARALLEL=1` with `WEB_TAG_WORKERS=4` (compose default) speeds up initial data preparation. The UI falls back to sequential tagging if workers fail to start. +- `WEB_VIRTUALIZE=1` enables virtualized grids in Stage 5 and the Owned library, smoothing large decks or libraries. +- Diagnostics overlays: enable `SHOW_DIAGNOSTICS=1`, then press **v** inside a virtualized grid to inspect render ranges, row counts, and paint timings. + +## Diagnostics and logs +- `SHOW_DIAGNOSTICS=1` unlocks the `/diagnostics` page with system summaries (`/status/sys`), feature flags, and per-request `X-Request-ID` headers. +- `SHOW_LOGS=1` turns on the `/logs` viewer with level & keyword filters, auto-refresh, and copy-to-clipboard. +- Health probes live at `/healthz` and return `{status, version, uptime_seconds}` for integration with uptime monitors.